diff --git a/.gitignore b/.gitignore index 5a965b494c..747f613a4b 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ android/**/bin android/**/src/main/res/values/libs.xml android/**/src/main/assets android/**/gradle* +*.class # VSCode # List taken from Github Global Ignores master@435c4d92 @@ -98,12 +99,9 @@ tools/jsdoc/package-lock.json # Python compile artifacts **/__pycache__ -# ignore unneeded unity project files for avatar exporter -tools/unity-avatar-exporter/Library -tools/unity-avatar-exporter/Logs -tools/unity-avatar-exporter/Packages -tools/unity-avatar-exporter/ProjectSettings -tools/unity-avatar-exporter/Temp +# ignore local unity project files for avatar exporter +tools/unity-avatar-exporter + server-console/package-lock.json vcpkg/ /tools/nitpick/compiledResources diff --git a/BUILD_QUEST.md b/BUILD_QUEST.md new file mode 100644 index 0000000000..e093969f83 --- /dev/null +++ b/BUILD_QUEST.md @@ -0,0 +1,65 @@ +Please read the [general build guide](BUILD.md) for information on building other platform. Only Quest specific instructions are found in this file. + +# Dependencies + +Building is currently supported on OSX, Windows and Linux platforms, but developers intending to do work on the library dependencies are strongly urged to use 64 bit Linux as a build platform + +You will need the following tools to build Android targets. + +* [Android Studio](https://developer.android.com/studio/index.html) + +### Android Studio + +Download the Android Studio installer and run it. Once installed, at the welcome screen, click configure in the lower right corner and select SDK manager + +From the SDK Platforms tab, select API levels 24 and 26. + +From the SDK Tools tab select the following + +* Android SDK Build-Tools +* GPU Debugging Tools +* CMake (even if you have a separate CMake installation) +* LLDB +* Android SDK Platform-Tools +* Android SDK Tools +* NDK (even if you have the NDK installed separately) + +Make sure the NDK installed version is 18 (or higher) + +# Environment + +Setting up the environment for android builds requires some additional steps + +#### Set up machine specific Gradle properties + +Create a `gradle.properties` file in $HOME/.gradle. Edit the file to contain the following + + HIFI_ANDROID_PRECOMPILED=/Android/hifi_externals + HIFI_ANDROID_KEYSTORE=/.jks + HIFI_ANDROID_KEYSTORE_PASSWORD= + HIFI_ANDROID_KEY_ALIAS= + HIFI_ANDROID_KEY_PASSWORD= + +Note, do not use `$HOME` for the path. It must be a fully qualified path name. + +### Setup the repository + +Clone the repository + +`git clone https://github.com/highfidelity/hifi.git` + +Enter the repository `android` directory + +`cd hifi/android` + +# Building & Running + +* Open Android Studio +* Choose _Open Existing Android Studio Project_ +* Navigate to the `hifi` repository and choose the `android` folder and select _OK_ +* Open Gradle.settings and comment out any projects not necessary +* From _File_ menu select _Sync with File System_ to resync Gradle settings +* From the _Build_ menu select _Make Project_ +* From +* Once the build completes, from the _Run_ menu select _Run App_ + diff --git a/CMakeLists.txt b/CMakeLists.txt index c8710eed05..1ba5e1264f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,6 +52,7 @@ else() set(MOBILE 0) endif() +set(HIFI_USE_OPTIMIZED_IK OFF) set(BUILD_CLIENT_OPTION ON) set(BUILD_SERVER_OPTION ON) set(BUILD_TESTS_OPTION OFF) @@ -100,6 +101,13 @@ if (ANDROID) add_definitions(-DCUSTOM_DISPLAY_PLUGINS) set(PLATFORM_PLUGIN_LIBRARIES oculusMobile oculusMobilePlugin) endif() + + # Allow client code to use preprocessor macros to distinguish between quest and non-quest builds + if (${HIFI_ANDROID_APP} STREQUAL "questInterface") + add_definitions(-DANDROID_APP_QUEST_INTERFACE) + elseif(${HIFI_ANDROID_APP} STREQUAL "interface") + add_definitions(-DANDROID_APP_INTERFACE) + endif() else () set(PLATFORM_QT_COMPONENTS WebEngine Xml) endif () @@ -108,7 +116,7 @@ if (USE_GLES AND (NOT ANDROID)) set(DISABLE_QML_OPTION ON) endif() - +option(HIFI_USE_OPTIMIZED_IK "USE OPTIMIZED IK" ${HIFI_USE_OPTIMIZED_IK_OPTION}) option(BUILD_CLIENT "Build client components" ${BUILD_CLIENT_OPTION}) option(BUILD_SERVER "Build server components" ${BUILD_SERVER_OPTION}) option(BUILD_TESTS "Build tests" ${BUILD_TESTS_OPTION}) @@ -139,6 +147,7 @@ foreach(PLATFORM_QT_COMPONENT ${PLATFORM_QT_COMPONENTS}) list(APPEND PLATFORM_QT_LIBRARIES "Qt5::${PLATFORM_QT_COMPONENT}") endforeach() +MESSAGE(STATUS "USE OPTIMIZED IK: " ${HIFI_USE_OPTIMIZED_IK}) MESSAGE(STATUS "Build server: " ${BUILD_SERVER}) MESSAGE(STATUS "Build client: " ${BUILD_CLIENT}) MESSAGE(STATUS "Build tests: " ${BUILD_TESTS}) @@ -184,6 +193,10 @@ find_package( Threads ) add_definitions(-DGLM_FORCE_RADIANS) add_definitions(-DGLM_ENABLE_EXPERIMENTAL) add_definitions(-DGLM_FORCE_CTOR_INIT) +if (HIFI_USE_OPTIMIZED_IK) + MESSAGE(STATUS "SET THE USE IK DEFINITION ") + add_definitions(-DHIFI_USE_OPTIMIZED_IK) +endif() set(HIFI_LIBRARY_DIR "${CMAKE_CURRENT_SOURCE_DIR}/libraries") set(EXTERNAL_PROJECT_PREFIX "project") diff --git a/android/apps/interface/build.gradle b/android/apps/interface/build.gradle index 4163df03b7..bb2745ca22 100644 --- a/android/apps/interface/build.gradle +++ b/android/apps/interface/build.gradle @@ -36,11 +36,6 @@ apply plugin: 'com.android.application' android { compileSdkVersion 26 - //buildToolsVersion '27.0.3' - - def appVersionCode = Integer.valueOf(VERSION_CODE ?: 1) - def appVersionName = RELEASE_NUMBER ?: "1.0" - defaultConfig { applicationId "io.highfidelity.hifiinterface" minSdkVersion 24 @@ -66,10 +61,10 @@ android { } signingConfigs { release { - storeFile project.hasProperty("HIFI_ANDROID_KEYSTORE") ? file(HIFI_ANDROID_KEYSTORE) : null - storePassword project.hasProperty("HIFI_ANDROID_KEYSTORE_PASSWORD") ? HIFI_ANDROID_KEYSTORE_PASSWORD : '' - keyAlias project.hasProperty("HIFI_ANDROID_KEY_ALIAS") ? HIFI_ANDROID_KEY_ALIAS : '' - keyPassword project.hasProperty("HIFI_ANDROID_KEY_PASSWORD") ? HIFI_ANDROID_KEY_PASSWORD : '' + storeFile project.hasProperty("HIFI_ANDROID_KEYSTORE") ? file(HIFI_ANDROID_KEYSTORE) : file('../keystore.jks') + storePassword project.hasProperty("HIFI_ANDROID_KEYSTORE_PASSWORD") ? HIFI_ANDROID_KEYSTORE_PASSWORD : 'password' + keyAlias project.hasProperty("HIFI_ANDROID_KEY_ALIAS") ? HIFI_ANDROID_KEY_ALIAS : 'key0' + keyPassword project.hasProperty("HIFI_ANDROID_KEY_PASSWORD") ? HIFI_ANDROID_KEY_PASSWORD : 'password' } } } @@ -90,10 +85,7 @@ android { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - signingConfig project.hasProperty("HIFI_ANDROID_KEYSTORE") && - project.hasProperty("HIFI_ANDROID_KEYSTORE_PASSWORD") && - project.hasProperty("HIFI_ANDROID_KEY_ALIAS") && - project.hasProperty("HIFI_ANDROID_KEY_PASSWORD")? signingConfigs.release : null + signingConfig signingConfigs.release buildConfigField "String", "BACKTRACE_URL", "\"" + (System.getenv("CMAKE_BACKTRACE_URL") ? System.getenv("CMAKE_BACKTRACE_URL") : '') + "\"" buildConfigField "String", "BACKTRACE_TOKEN", "\"" + (System.getenv("CMAKE_BACKTRACE_TOKEN") ? System.getenv("CMAKE_BACKTRACE_TOKEN") : '') + "\"" buildConfigField "String", "OAUTH_CLIENT_ID", "\"" + (System.getenv("OAUTH_CLIENT_ID") ? System.getenv("OAUTH_CLIENT_ID") : '') + "\"" diff --git a/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java index a7bda3c29b..3cdb9f5a09 100644 --- a/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java +++ b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java @@ -81,6 +81,7 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW private boolean nativeEnterBackgroundCallEnqueued = false; private SlidingDrawer mWebSlidingDrawer; private boolean mStartInDomain; + private boolean isLoading; // private GvrApi gvrApi; // Opaque native pointer to the Application C++ object. // This object is owned by the InterfaceActivity instance and passed to the native methods. @@ -94,7 +95,7 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW @Override public void onCreate(Bundle savedInstanceState) { - super.isLoading = true; + isLoading = true; Intent intent = getIntent(); if (intent.hasExtra(DOMAIN_URL) && !TextUtils.isEmpty(intent.getStringExtra(DOMAIN_URL))) { intent.putExtra("applicationArguments", "--url " + intent.getStringExtra(DOMAIN_URL)); @@ -145,7 +146,7 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW @Override protected void onPause() { super.onPause(); - if (super.isLoading) { + if (isLoading) { nativeEnterBackgroundCallEnqueued = true; } else { nativeEnterBackground(); @@ -172,7 +173,6 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW super.onResume(); nativeEnterForeground(); surfacesWorkaround(); - keepInterfaceRunning = false; registerReceiver(headsetStateReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG)); //gvrApi.resumeTracking(); } @@ -382,7 +382,7 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW } public void onAppLoadedComplete() { - super.isLoading = false; + isLoading = false; if (nativeEnterBackgroundCallEnqueued) { nativeEnterBackground(); } @@ -413,7 +413,6 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW @Override public void onExpand() { - keepInterfaceRunning = true; } @Override diff --git a/android/apps/keystore.jks b/android/apps/keystore.jks new file mode 100644 index 0000000000..4b646122f6 Binary files /dev/null and b/android/apps/keystore.jks differ diff --git a/android/apps/questFramePlayer/CMakeLists.txt b/android/apps/questFramePlayer/CMakeLists.txt index 5889585a6c..ea60e27a7d 100644 --- a/android/apps/questFramePlayer/CMakeLists.txt +++ b/android/apps/questFramePlayer/CMakeLists.txt @@ -1,7 +1,7 @@ set(TARGET_NAME questFramePlayer) setup_hifi_library(AndroidExtras) link_hifi_libraries(shared ktx shaders gpu gl oculusMobile ${PLATFORM_GL_BACKEND}) -target_include_directories(${TARGET_NAME} PRIVATE ${HIFI_ANDROID_PRECOMPILED}/ovr/VrApi/Include) + target_link_libraries(${TARGET_NAME} android log m) target_opengl() target_oculus_mobile() diff --git a/android/apps/questFramePlayer/src/main/AndroidManifest.xml b/android/apps/questFramePlayer/src/main/AndroidManifest.xml index 721e8cee89..ba14b04532 100644 --- a/android/apps/questFramePlayer/src/main/AndroidManifest.xml +++ b/android/apps/questFramePlayer/src/main/AndroidManifest.xml @@ -19,24 +19,6 @@ android:name="org.qtproject.qt5.android.bindings.QtApplication" tools:ignore="GoogleAppIndexingWarning,MissingApplicationIcon"> - - - - - - - - - + + + + + + + diff --git a/android/apps/questFramePlayer/src/main/cpp/AndroidHelper.cpp b/android/apps/questFramePlayer/src/main/cpp/AndroidHelper.cpp new file mode 100644 index 0000000000..797040ab69 --- /dev/null +++ b/android/apps/questFramePlayer/src/main/cpp/AndroidHelper.cpp @@ -0,0 +1,30 @@ +// +// Created by Bradley Austin Davis on 2019/02/15 +// Copyright 2013-2019 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 "AndroidHelper.h" + +#include +#include + +AndroidHelper::AndroidHelper() { +} + +AndroidHelper::~AndroidHelper() { +} + +void AndroidHelper::notifyLoadComplete() { + emit qtAppLoadComplete(); +} + +void AndroidHelper::notifyEnterForeground() { + emit enterForeground(); +} + +void AndroidHelper::notifyEnterBackground() { + emit enterBackground(); +} + diff --git a/android/apps/questFramePlayer/src/main/cpp/AndroidHelper.h b/android/apps/questFramePlayer/src/main/cpp/AndroidHelper.h new file mode 100644 index 0000000000..ef6722462c --- /dev/null +++ b/android/apps/questFramePlayer/src/main/cpp/AndroidHelper.h @@ -0,0 +1,43 @@ +// +// Created by Bradley Austin Davis on 2019/02/15 +// Copyright 2013-2019 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_Android_Helper_h +#define hifi_Android_Helper_h + +#include +#include +#include +#include + +class AndroidHelper : public QObject { + Q_OBJECT +public: + AndroidHelper(AndroidHelper const&) = delete; + void operator=(AndroidHelper const&) = delete; + + static AndroidHelper& instance() { + static AndroidHelper instance; + return instance; + } + + void notifyLoadComplete(); + void notifyEnterForeground(); + void notifyEnterBackground(); + + +signals: + void qtAppLoadComplete(); + void enterForeground(); + void enterBackground(); + +private: + AndroidHelper(); + ~AndroidHelper(); +}; + +#endif diff --git a/android/apps/questFramePlayer/src/main/cpp/PlayerWindow.cpp b/android/apps/questFramePlayer/src/main/cpp/PlayerWindow.cpp index ec2986298e..8f78b1946a 100644 --- a/android/apps/questFramePlayer/src/main/cpp/PlayerWindow.cpp +++ b/android/apps/questFramePlayer/src/main/cpp/PlayerWindow.cpp @@ -11,15 +11,11 @@ #include PlayerWindow::PlayerWindow() { - installEventFilter(this); - setFlags(Qt::MSWindowsOwnDC | Qt::Window | Qt::Dialog | Qt::WindowMinMaxButtonsHint | Qt::WindowTitleHint); + setFlags(Qt::Window); setSurfaceType(QSurface::OpenGLSurface); create(); showFullScreen(); // Ensure the window is visible and the GL context is valid QCoreApplication::processEvents(); - _renderThread.initialize(this); -} - -PlayerWindow::~PlayerWindow() { + _renderThread.initialize(); } diff --git a/android/apps/questFramePlayer/src/main/cpp/PlayerWindow.h b/android/apps/questFramePlayer/src/main/cpp/PlayerWindow.h index e4dd6cef43..5e7dc82781 100644 --- a/android/apps/questFramePlayer/src/main/cpp/PlayerWindow.h +++ b/android/apps/questFramePlayer/src/main/cpp/PlayerWindow.h @@ -8,22 +8,13 @@ #pragma once #include -#include - -#include #include "RenderThread.h" -// Create a simple OpenGL window that renders text in various ways class PlayerWindow : public QWindow { public: PlayerWindow(); - virtual ~PlayerWindow(); - -protected: - //bool eventFilter(QObject* obj, QEvent* event) override; - //void keyPressEvent(QKeyEvent* event) override; + virtual ~PlayerWindow() {} private: - QSettings _settings; RenderThread _renderThread; }; diff --git a/android/apps/questFramePlayer/src/main/cpp/RenderThread.cpp b/android/apps/questFramePlayer/src/main/cpp/RenderThread.cpp index 5eabe6b9b1..78a4487284 100644 --- a/android/apps/questFramePlayer/src/main/cpp/RenderThread.cpp +++ b/android/apps/questFramePlayer/src/main/cpp/RenderThread.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -29,9 +30,7 @@ #include #include -static JNIEnv* _env { nullptr }; -static JavaVM* _vm { nullptr }; -static jobject _activity { nullptr }; +#include "AndroidHelper.h" struct HandController{ ovrInputTrackedRemoteCapabilities caps {}; @@ -48,21 +47,43 @@ struct HandController{ }; std::vector devices; +QAndroidJniObject __interfaceActivity; extern "C" { -JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *, void *) { +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void *) { __android_log_write(ANDROID_LOG_WARN, "QQQ", __FUNCTION__); return JNI_VERSION_1_6; } +JNIEXPORT void JNICALL +Java_io_highfidelity_oculus_OculusMobileActivity_questNativeOnCreate(JNIEnv *env, jobject obj) { + __android_log_print(ANDROID_LOG_INFO, "QQQ", __FUNCTION__); + __interfaceActivity = QAndroidJniObject(obj); + QObject::connect(&AndroidHelper::instance(), &AndroidHelper::qtAppLoadComplete, []() { + __interfaceActivity.callMethod("onAppLoadedComplete", "()V"); + QObject::disconnect(&AndroidHelper::instance(), &AndroidHelper::qtAppLoadComplete, nullptr, nullptr); + }); +} -JNIEXPORT void JNICALL Java_io_highfidelity_frameplayer_QuestQtActivity_nativeOnCreate(JNIEnv* env, jobject obj) { - env->GetJavaVM(&_vm); - _activity = env->NewGlobalRef(obj); +JNIEXPORT void +Java_io_highfidelity_oculus_OculusMobileActivity_questOnAppAfterLoad(JNIEnv *env, jobject obj) { + AndroidHelper::instance().moveToThread(qApp->thread()); } + +JNIEXPORT void JNICALL +Java_io_highfidelity_oculus_OculusMobileActivity_questNativeOnPause(JNIEnv *env, jobject obj) { + AndroidHelper::instance().notifyEnterBackground(); } +JNIEXPORT void JNICALL +Java_io_highfidelity_oculus_OculusMobileActivity_questNativeOnResume(JNIEnv *env, jobject obj) { + AndroidHelper::instance().notifyEnterForeground(); +} + +} + + static const char* FRAME_FILE = "assets:/frames/20190121_1220.json"; static void textureLoader(const std::string& filename, const gpu::TexturePointer& texture, uint16_t layer) { @@ -84,11 +105,10 @@ void RenderThread::move(const glm::vec3& v) { _correction = glm::inverse(glm::translate(mat4(), v)) * _correction; } -void RenderThread::initialize(QWindow* window) { +void RenderThread::initialize() { std::unique_lock lock(_frameLock); setObjectName("RenderThread"); Parent::initialize(); - _window = window; _thread->setObjectName("RenderThread"); } @@ -96,14 +116,7 @@ void RenderThread::setup() { // Wait until the context has been moved to this thread { std::unique_lock lock(_frameLock); } - ovr::VrHandler::initVr(); - __android_log_write(ANDROID_LOG_WARN, "QQQ", "Launching oculus activity"); - _vm->AttachCurrentThread(&_env, nullptr); - jclass cls = _env->GetObjectClass(_activity); - jmethodID mid = _env->GetMethodID(cls, "launchOculusActivity", "()V"); - _env->CallVoidMethod(_activity, mid); - __android_log_write(ANDROID_LOG_WARN, "QQQ", "Launching oculus activity done"); ovr::VrHandler::setHandler(this); makeCurrent(); @@ -169,7 +182,6 @@ void RenderThread::handleInput() { const auto &remote = controller.state; if (remote.Joystick.x != 0.0f || remote.Joystick.y != 0.0f) { glm::vec3 translation; - float rotation = 0.0f; if (caps.ControllerCapabilities & ovrControllerCaps_LeftHand) { translation = glm::vec3{0.0f, -remote.Joystick.y, 0.0f}; } else { diff --git a/android/apps/questFramePlayer/src/main/cpp/RenderThread.h b/android/apps/questFramePlayer/src/main/cpp/RenderThread.h index 701cd25f5b..747d0d9e8d 100644 --- a/android/apps/questFramePlayer/src/main/cpp/RenderThread.h +++ b/android/apps/questFramePlayer/src/main/cpp/RenderThread.h @@ -20,11 +20,9 @@ class RenderThread : public GenericThread, ovr::VrHandler { using Parent = GenericThread; public: - QWindow* _window{ nullptr }; std::mutex _mutex; gpu::ContextPointer _gpuContext; // initialized during window creation std::shared_ptr _backend; - std::atomic _presentCount{ 0 }; std::mutex _frameLock; std::queue _pendingFrames; gpu::FramePointer _activeFrame; @@ -39,6 +37,6 @@ public: void handleInput(); void submitFrame(const gpu::FramePointer& frame); - void initialize(QWindow* window); + void initialize(); void renderFrame(); }; diff --git a/android/apps/questFramePlayer/src/main/cpp/main.cpp b/android/apps/questFramePlayer/src/main/cpp/main.cpp index 4730d3fa15..123ba904f4 100644 --- a/android/apps/questFramePlayer/src/main/cpp/main.cpp +++ b/android/apps/questFramePlayer/src/main/cpp/main.cpp @@ -11,30 +11,33 @@ #include #include #include +#include #include #include "PlayerWindow.h" +#include "AndroidHelper.h" + void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { if (!message.isEmpty()) { - const char * local=message.toStdString().c_str(); + const char* local = message.toStdString().c_str(); switch (type) { case QtDebugMsg: - __android_log_write(ANDROID_LOG_DEBUG,"Interface",local); + __android_log_write(ANDROID_LOG_DEBUG, "Interface", local); break; case QtInfoMsg: - __android_log_write(ANDROID_LOG_INFO,"Interface",local); + __android_log_write(ANDROID_LOG_INFO, "Interface", local); break; case QtWarningMsg: - __android_log_write(ANDROID_LOG_WARN,"Interface",local); + __android_log_write(ANDROID_LOG_WARN, "Interface", local); break; case QtCriticalMsg: - __android_log_write(ANDROID_LOG_ERROR,"Interface",local); + __android_log_write(ANDROID_LOG_ERROR, "Interface", local); break; case QtFatalMsg: default: - __android_log_write(ANDROID_LOG_FATAL,"Interface",local); + __android_log_write(ANDROID_LOG_FATAL, "Interface", local); abort(); } } @@ -46,11 +49,13 @@ int main(int argc, char** argv) { auto oldMessageHandler = qInstallMessageHandler(messageHandler); DependencyManager::set(); PlayerWindow window; - __android_log_write(ANDROID_LOG_FATAL,"QQQ","Exec"); + QTimer::singleShot(10, []{ + __android_log_write(ANDROID_LOG_WARN, "QQQ", "notifyLoadComplete"); + AndroidHelper::instance().notifyLoadComplete(); + }); + __android_log_write(ANDROID_LOG_WARN, "QQQ", "Exec"); app.exec(); - __android_log_write(ANDROID_LOG_FATAL,"QQQ","Exec done"); + __android_log_write(ANDROID_LOG_WARN, "QQQ", "Exec done"); qInstallMessageHandler(oldMessageHandler); return 0; } - - diff --git a/android/apps/questFramePlayer/src/main/java/io/highfidelity/frameplayer/QuestQtActivity.java b/android/apps/questFramePlayer/src/main/java/io/highfidelity/frameplayer/QuestQtActivity.java deleted file mode 100644 index d498e27547..0000000000 --- a/android/apps/questFramePlayer/src/main/java/io/highfidelity/frameplayer/QuestQtActivity.java +++ /dev/null @@ -1,53 +0,0 @@ -// -// Created by Bradley Austin Davis on 2018/11/20 -// Copyright 2013-2018 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 -// -package io.highfidelity.frameplayer; - -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; - -import org.qtproject.qt5.android.bindings.QtActivity; - -import io.highfidelity.oculus.OculusMobileActivity; - - -public class QuestQtActivity extends QtActivity { - private native void nativeOnCreate(); - private boolean launchedQuestMode = false; - - @Override - public void onCreate(Bundle savedInstanceState) { - Log.w("QQQ_Qt", "QuestQtActivity::onCreate"); - super.onCreate(savedInstanceState); - nativeOnCreate(); - } - - @Override - public void onDestroy() { - Log.w("QQQ_Qt", "QuestQtActivity::onDestroy"); - super.onDestroy(); - } - - public void launchOculusActivity() { - Log.w("QQQ_Qt", "QuestQtActivity::launchOculusActivity"); - runOnUiThread(()->{ - keepInterfaceRunning = true; - launchedQuestMode = true; - moveTaskToBack(true); - startActivity(new Intent(this, QuestRenderActivity.class)); - }); - } - - @Override - public void onResume() { - super.onResume(); - if (launchedQuestMode) { - moveTaskToBack(true); - } - } -} diff --git a/android/apps/questFramePlayer/src/main/java/io/highfidelity/frameplayer/QuestRenderActivity.java b/android/apps/questFramePlayer/src/main/java/io/highfidelity/frameplayer/QuestRenderActivity.java index a395a32b68..9e7c0ab973 100644 --- a/android/apps/questFramePlayer/src/main/java/io/highfidelity/frameplayer/QuestRenderActivity.java +++ b/android/apps/questFramePlayer/src/main/java/io/highfidelity/frameplayer/QuestRenderActivity.java @@ -1,14 +1,6 @@ package io.highfidelity.frameplayer; -import android.content.Intent; -import android.os.Bundle; - import io.highfidelity.oculus.OculusMobileActivity; public class QuestRenderActivity extends OculusMobileActivity { - @Override - public void onCreate(Bundle savedState) { - super.onCreate(savedState); - startActivity(new Intent(this, QuestQtActivity.class)); - } } diff --git a/android/apps/questInterface/CMakeLists.txt b/android/apps/questInterface/CMakeLists.txt new file mode 100644 index 0000000000..97ca46f6e5 --- /dev/null +++ b/android/apps/questInterface/CMakeLists.txt @@ -0,0 +1,16 @@ +set(TARGET_NAME questInterface) +setup_hifi_library() +link_hifi_libraries( + shared task networking qml + image fbx hfm render-utils physics entities octree + oculusMobile oculusMobilePlugin + gl gpu ${PLATFORM_GL_BACKEND} +) +target_opengl() +target_bullet() +target_oculus_mobile() + +add_subdirectory("${CMAKE_SOURCE_DIR}/interface" "libraries/interface") +include_directories("${CMAKE_SOURCE_DIR}/interface/src") +add_subdirectory("${CMAKE_SOURCE_DIR}/plugins/hifiCodec" "libraries/hifiCodecPlugin") +target_link_libraries(questInterface android log m interface) diff --git a/android/apps/questInterface/build.gradle b/android/apps/questInterface/build.gradle new file mode 100644 index 0000000000..c54401a81b --- /dev/null +++ b/android/apps/questInterface/build.gradle @@ -0,0 +1,142 @@ +import org.apache.tools.ant.taskdefs.condition.Os +apply plugin: 'com.android.application' + +task renameHifiACTaskDebug() { + doLast { + def sourceFile = new File("${appDir}/build/intermediates/cmake/debug/obj/arm64-v8a/","libhifiCodec.so") + def destinationFile = new File("${appDir}/src/main/jniLibs/arm64-v8a", "libplugins_libhifiCodec.so") + copy { from sourceFile; into destinationFile.parent; rename(sourceFile.name, destinationFile.name) } + } +} +task renameHifiACTaskRelease(type: Copy) { + doLast { + def sourceFile = new File("${appDir}/build/intermediates/cmake/release/obj/arm64-v8a/","libhifiCodec.so") + def destinationFile = new File("${appDir}/src/main/jniLibs/arm64-v8a", "libplugins_libhifiCodec.so") + copy { from sourceFile; into destinationFile.parent; rename(sourceFile.name, destinationFile.name) } + } +} + +android { + compileSdkVersion 28 + defaultConfig { + applicationId "io.highfidelity.questInterface" + minSdkVersion 24 + targetSdkVersion 28 + versionCode appVersionCode + versionName appVersionName + ndk { abiFilters 'arm64-v8a' } + externalNativeBuild { + cmake { + arguments '-DHIFI_ANDROID=1', + '-DHIFI_ANDROID_APP=questInterface', + '-DANDROID_TOOLCHAIN=clang', + '-DANDROID_STL=c++_shared', + '-DCMAKE_VERBOSE_MAKEFILE=ON', + '-DRELEASE_NUMBER=' + RELEASE_NUMBER, + '-DRELEASE_TYPE=' + RELEASE_TYPE, + '-DSTABLE_BUILD=' + STABLE_BUILD, + '-DDISABLE_QML=OFF', + '-DDISABLE_KTX_CACHE=OFF', + '-DUSE_BREAKPAD=OFF' + targets = ['questInterface'] + } + } + signingConfigs { + release { + storeFile project.hasProperty("HIFI_ANDROID_KEYSTORE") ? file(HIFI_ANDROID_KEYSTORE) : file('../keystore.jks') + storePassword project.hasProperty("HIFI_ANDROID_KEYSTORE_PASSWORD") ? HIFI_ANDROID_KEYSTORE_PASSWORD : 'password' + keyAlias project.hasProperty("HIFI_ANDROID_KEY_ALIAS") ? HIFI_ANDROID_KEY_ALIAS : 'key0' + keyPassword project.hasProperty("HIFI_ANDROID_KEY_PASSWORD") ? HIFI_ANDROID_KEY_PASSWORD : 'password' + v2SigningEnabled false + } + } + } + + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + buildTypes { + debug { + buildConfigField "String", "BACKTRACE_URL", "\"" + (System.getenv("CMAKE_BACKTRACE_URL") ? System.getenv("CMAKE_BACKTRACE_URL") : '') + "\"" + buildConfigField "String", "BACKTRACE_TOKEN", "\"" + (System.getenv("CMAKE_BACKTRACE_TOKEN") ? System.getenv("CMAKE_BACKTRACE_TOKEN") : '') + "\"" + buildConfigField "String", "OAUTH_CLIENT_ID", "\"" + (System.getenv("OAUTH_CLIENT_ID") ? System.getenv("OAUTH_CLIENT_ID") : '') + "\"" + buildConfigField "String", "OAUTH_CLIENT_SECRET", "\"" + (System.getenv("OAUTH_CLIENT_SECRET") ? System.getenv("OAUTH_CLIENT_SECRET") : '') + "\"" + buildConfigField "String", "OAUTH_REDIRECT_URI", "\"" + (System.getenv("OAUTH_REDIRECT_URI") ? System.getenv("OAUTH_REDIRECT_URI") : '') + "\"" + } + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.release + buildConfigField "String", "BACKTRACE_URL", "\"" + (System.getenv("CMAKE_BACKTRACE_URL") ? System.getenv("CMAKE_BACKTRACE_URL") : '') + "\"" + buildConfigField "String", "BACKTRACE_TOKEN", "\"" + (System.getenv("CMAKE_BACKTRACE_TOKEN") ? System.getenv("CMAKE_BACKTRACE_TOKEN") : '') + "\"" + buildConfigField "String", "OAUTH_CLIENT_ID", "\"" + (System.getenv("OAUTH_CLIENT_ID") ? System.getenv("OAUTH_CLIENT_ID") : '') + "\"" + buildConfigField "String", "OAUTH_CLIENT_SECRET", "\"" + (System.getenv("OAUTH_CLIENT_SECRET") ? System.getenv("OAUTH_CLIENT_SECRET") : '') + "\"" + buildConfigField "String", "OAUTH_REDIRECT_URI", "\"" + (System.getenv("OAUTH_REDIRECT_URI") ? System.getenv("OAUTH_REDIRECT_URI") : '') + "\"" + } + } + + externalNativeBuild { + cmake { + path '../../../CMakeLists.txt' + } + } + + applicationVariants.all { variant -> + // Our asset contents depend on items produced in the CMake build + // so our merge has to depend on the external native build + variant.externalNativeBuildTasks.each { task -> + variant.mergeResources.dependsOn(task) + if (Os.isFamily(Os.FAMILY_UNIX)) { + // FIXME + def uploadDumpSymsTask = rootProject.getTasksByName("uploadBreakpadDumpSyms${variant.name.capitalize()}", false).first() + def runDumpSymsTask = rootProject.getTasksByName("runBreakpadDumpSyms${variant.name.capitalize()}", false).first() + def renameHifiACTask = rootProject.getTasksByName("renameHifiACTask${variant.name.capitalize()}", false).first() + runDumpSymsTask.dependsOn(task) + variant.assemble.dependsOn(uploadDumpSymsTask) + variant.mergeResources.dependsOn(renameHifiACTask) + } + } + + variant.mergeAssets.doLast { + def assetList = new LinkedList() + def youngestLastModified = 0 + + // Copy the compiled resources generated by the external native build + copy { + from new File(projectDir, "../../../interface/compiledResources") + into outputDir + duplicatesStrategy DuplicatesStrategy.INCLUDE + eachFile { details -> + youngestLastModified = Math.max(youngestLastModified, details.lastModified) + assetList.add(details.path) + } + } + + // Copy the scripts directory + copy { + from new File(projectDir, "../../../scripts") + into new File(outputDir, "scripts") + duplicatesStrategy DuplicatesStrategy.INCLUDE + eachFile { details-> + youngestLastModified = Math.max(youngestLastModified, details.lastModified) + assetList.add("scripts/" + details.path) + } + } + + // Write a list of files to be unpacked to the cache folder + new File(outputDir, 'cache_assets.txt').withWriter { out -> + out.println(Long.toString(youngestLastModified)) + assetList.each { file -> out.println(file) } + } + } + } +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: '../../libraries/qt/libs') + implementation project(':oculus') + implementation project(':qt') +} diff --git a/android/apps/questInterface/proguard-rules.pro b/android/apps/questInterface/proguard-rules.pro new file mode 100644 index 0000000000..b3c0078513 --- /dev/null +++ b/android/apps/questInterface/proguard-rules.pro @@ -0,0 +1,25 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in C:\Android\SDK/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/android/apps/questInterface/src/main/AndroidManifest.xml b/android/apps/questInterface/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..a5de47bdce --- /dev/null +++ b/android/apps/questInterface/src/main/AndroidManifest.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/apps/questInterface/src/main/cpp/native.cpp b/android/apps/questInterface/src/main/cpp/native.cpp new file mode 100644 index 0000000000..547874b84e --- /dev/null +++ b/android/apps/questInterface/src/main/cpp/native.cpp @@ -0,0 +1,110 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include +#include + +void initOculusPlatform(JNIEnv* env, jobject obj) { + static std::once_flag once; + std::call_once(once, [&]{ + // static const char* appID = "2343652845669354"; + // if (ovr_PlatformInitializeAndroid(appID, obj, env) != ovrPlatformInitialize_Success) { + // __android_log_write(ANDROID_LOG_WARN, "QQQ", "Failed to init platform SDK"); + // return; + // } + // ovr_Voip_SetSystemVoipSuppressed(true); + }); +} + +void getClassName(JNIEnv *env, jobject obj){ + jclass cls = env->GetObjectClass(obj); + jmethodID mid = env->GetMethodID(cls,"getClass", "()Ljava/lang/Class;"); + jobject clsObj = env->CallObjectMethod(obj, mid); + + cls= env->GetObjectClass(clsObj); + + mid= env->GetMethodID(cls, "getName", "()Ljava/lang/String;"); + + jstring strObj = (jstring) env->CallObjectMethod(clsObj, mid); + + const char* str = env->GetStringUTFChars(strObj, NULL); + + __android_log_print(ANDROID_LOG_ERROR,__FUNCTION__, "Native Class call: %s",str); + + env->ReleaseStringUTFChars(strObj, str); +} + + +extern "C" { + JNIEXPORT void JNICALL + Java_io_highfidelity_oculus_OculusMobileActivity_nativeInitOculusPlatform(JNIEnv *env, jobject obj){ + initOculusPlatform(env, obj); + } + QAndroidJniObject __interfaceActivity; + + JNIEXPORT void JNICALL + Java_io_highfidelity_oculus_OculusMobileActivity_questNativeOnCreate(JNIEnv *env, jobject obj) { + __android_log_print(ANDROID_LOG_INFO, "QQQ", __FUNCTION__); + initOculusPlatform(env, obj); + getClassName(env, obj); + + __interfaceActivity = QAndroidJniObject(obj); + + QObject::connect(&AndroidHelper::instance(), &AndroidHelper::qtAppLoadComplete, []() { + __interfaceActivity.callMethod("onAppLoadedComplete", "()V"); + + QObject::disconnect(&AndroidHelper::instance(), &AndroidHelper::qtAppLoadComplete, + nullptr, + nullptr); + }); + } + + JNIEXPORT void JNICALL + Java_io_highfidelity_oculus_OculusMobileActivity_questNativeAwayMode(JNIEnv *env, jobject obj) { + AndroidHelper::instance().toggleAwayMode(); + } + + +JNIEXPORT void Java_io_highfidelity_oculus_OculusMobileActivity_questOnAppAfterLoad(JNIEnv* env, jobject obj) { + AndroidHelper::instance().moveToThread(qApp->thread()); +} + + JNIEXPORT void JNICALL + Java_io_highfidelity_oculus_OculusMobileActivity_questNativeOnPause(JNIEnv *env, jobject obj) { + AndroidHelper::instance().notifyEnterBackground(); + } + + JNIEXPORT void JNICALL + Java_io_highfidelity_oculus_OculusMobileActivity_questNativeOnResume(JNIEnv *env, jobject obj) { + AndroidHelper::instance().notifyEnterForeground(); + } + + JNIEXPORT void JNICALL + Java_io_highfidelity_questInterface_receiver_HeadsetStateReceiver_notifyHeadsetOn(JNIEnv *env, + jobject instance, + jboolean pluggedIn) { + AndroidHelper::instance().notifyHeadsetOn(pluggedIn); + } + +} diff --git a/android/apps/questInterface/src/main/java/io/highfidelity/questInterface/InterfaceActivity.java b/android/apps/questInterface/src/main/java/io/highfidelity/questInterface/InterfaceActivity.java new file mode 100644 index 0000000000..df05576ea9 --- /dev/null +++ b/android/apps/questInterface/src/main/java/io/highfidelity/questInterface/InterfaceActivity.java @@ -0,0 +1,14 @@ +package io.highfidelity.questInterface; + +import android.os.Bundle; +import io.highfidelity.oculus.OculusMobileActivity; +import io.highfidelity.utils.HifiUtils; + +public class InterfaceActivity extends OculusMobileActivity { + + @Override + public void onCreate(Bundle savedInstanceState) { + HifiUtils.upackAssets(getAssets(), getCacheDir().getAbsolutePath()); + super.onCreate(savedInstanceState); + } +} diff --git a/android/apps/questInterface/src/main/java/io/highfidelity/questInterface/PermissionsChecker.java b/android/apps/questInterface/src/main/java/io/highfidelity/questInterface/PermissionsChecker.java new file mode 100644 index 0000000000..c5739ef7ea --- /dev/null +++ b/android/apps/questInterface/src/main/java/io/highfidelity/questInterface/PermissionsChecker.java @@ -0,0 +1,84 @@ +package io.highfidelity.questInterface; + +import android.Manifest; +import android.app.Activity; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.text.TextUtils; + +import io.highfidelity.oculus.OculusMobileActivity; +import io.highfidelity.utils.HifiUtils; + +public class PermissionsChecker extends Activity { + private static final int REQUEST_PERMISSIONS = 20; + private static final String TAG = PermissionsChecker.class.getName(); + private static final String[] REQUIRED_PERMISSIONS = new String[]{ + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.RECORD_AUDIO, + Manifest.permission.CAMERA + }; + + private static final String EXTRA_ARGS = "args"; + private String mArgs; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mArgs =(getIntent().getStringExtra(EXTRA_ARGS)); + + if(!TextUtils.isEmpty(mArgs)) { + System.out.println("Application launched with following args: " + mArgs); + } + + requestAppPermissions(REQUIRED_PERMISSIONS,REQUEST_PERMISSIONS); + } + + public void requestAppPermissions(final String[] requestedPermissions, + final int requestCode) { + int permissionCheck = PackageManager.PERMISSION_GRANTED; + boolean shouldShowRequestPermissionRationale = false; + for (String permission : requestedPermissions) { + permissionCheck = permissionCheck + checkSelfPermission(permission); + shouldShowRequestPermissionRationale = shouldShowRequestPermissionRationale || shouldShowRequestPermissionRationale(permission); + } + if (permissionCheck != PackageManager.PERMISSION_GRANTED) { + System.out.println("Permission was not granted. Ask for permissions"); + if (shouldShowRequestPermissionRationale) { + requestPermissions(requestedPermissions, requestCode); + } else { + requestPermissions(requestedPermissions, requestCode); + } + } else { + System.out.println("Launching the other activity.."); + launchActivityWithPermissions(); + } + } + + private void launchActivityWithPermissions() { + Intent intent= new Intent(this, InterfaceActivity.class); + + if(!TextUtils.isEmpty(mArgs)) { + intent.putExtra("applicationArguments", mArgs); + } + + startActivity(intent); + finish(); + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + int permissionCheck = PackageManager.PERMISSION_GRANTED; + for (int permission : grantResults) { + permissionCheck = permissionCheck + permission; + } + if ((grantResults.length > 0) && permissionCheck == PackageManager.PERMISSION_GRANTED) { + launchActivityWithPermissions(); + } else if (grantResults.length > 0) { + System.out.println("User has deliberately denied Permissions. Launching anyways"); + launchActivityWithPermissions(); + } + } +} diff --git a/android/apps/questInterface/src/main/res/drawable/ic_launcher.xml b/android/apps/questInterface/src/main/res/drawable/ic_launcher.xml new file mode 100644 index 0000000000..03b1edc4e9 --- /dev/null +++ b/android/apps/questInterface/src/main/res/drawable/ic_launcher.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/android/apps/questInterface/src/main/res/values/strings.xml b/android/apps/questInterface/src/main/res/values/strings.xml new file mode 100644 index 0000000000..99e8d501ac --- /dev/null +++ b/android/apps/questInterface/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Interface + diff --git a/android/build.gradle b/android/build.gradle index ed2ca1c47e..5a4dbc0033 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -42,6 +42,8 @@ ext { RELEASE_TYPE = project.hasProperty('RELEASE_TYPE') ? project.getProperty('RELEASE_TYPE') : 'DEV' STABLE_BUILD = project.hasProperty('STABLE_BUILD') ? project.getProperty('STABLE_BUILD') : '0' EXEC_SUFFIX = Os.isFamily(Os.FAMILY_WINDOWS) ? '.exe' : '' + appVersionCode = Integer.valueOf(VERSION_CODE ?: 1) + appVersionName = RELEASE_NUMBER ?: "1.0" } def appDir = new File(projectDir, 'apps/interface') diff --git a/android/build_android.sh b/android/build_android.sh index a066332f9a..e9c69b09de 100755 --- a/android/build_android.sh +++ b/android/build_android.sh @@ -1,11 +1,35 @@ #!/usr/bin/env bash set -xeuo pipefail + +ANDROID_BUILD_TYPE=release +ANDROID_BUILD_TARGET=assembleRelease + +if [[ "$RELEASE_TYPE" == "PR" ]]; then +ANDROID_APK_SUFFIX=PR${RELEASE_NUMBER}-${SHA7}.apk ; +elif [[ "${STABLE_BUILD}" == "1" ]]; then +ANDROID_APK_SUFFIX=${RELEASE_NUMBER}.apk ; +else +ANDROID_APK_SUFFIX=${RELEASE_NUMBER}-${SHA7}.apk ; +fi + + +# Interface build +ANDROID_APP=interface +ANDROID_OUTPUT_DIR=./apps/${ANDROID_APP}/build/outputs/apk/${ANDROID_BUILD_TYPE} +ANDROID_OUTPUT_FILE=${ANDROID_APP}-${ANDROID_BUILD_TYPE}.apk +ANDROID_APK_NAME=HighFidelity-Beta-${ANDROID_APK_SUFFIX} ./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PVERSION_CODE=${VERSION_CODE} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} ${ANDROID_APP}:${ANDROID_BUILD_TARGET} +cp ${ANDROID_OUTPUT_DIR}/${ANDROID_OUTPUT_FILE} ./${ANDROID_APK_NAME} + +# Quest Interface build +ANDROID_APP=questInterface +ANDROID_OUTPUT_DIR=./apps/${ANDROID_APP}/build/outputs/apk/${ANDROID_BUILD_TYPE} +ANDROID_OUTPUT_FILE=${ANDROID_APP}-${ANDROID_BUILD_TYPE}.apk +ANDROID_APK_NAME=HighFidelity-Quest-Beta-${ANDROID_APK_SUFFIX} +./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PVERSION_CODE=${VERSION_CODE} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} ${ANDROID_APP}:${ANDROID_BUILD_TARGET} || true +cp ${ANDROID_OUTPUT_DIR}/${ANDROID_OUTPUT_FILE} ./${ANDROID_APK_NAME} || true + + + -# This is the actual output from gradle, which no longer attempts to muck with the naming of the APK -OUTPUT_APK=./apps/${ANDROID_APP}/build/outputs/apk/${ANDROID_BUILD_DIR}/${ANDROID_BUILT_APK_NAME} -# This is the APK name requested by Jenkins -TARGET_APK=./${ANDROID_APK_NAME} -# Make sure this matches up with the new ARTIFACT_EXPRESSION for jenkins builds, which should be "android/*.apk" -cp ${OUTPUT_APK} ${TARGET_APK} diff --git a/android/containerized_build.sh b/android/containerized_build.sh index 8b2f26cb50..1ca597b2b9 100755 --- a/android/containerized_build.sh +++ b/android/containerized_build.sh @@ -9,6 +9,11 @@ docker build --build-arg BUILD_UID=`id -u` -t "${DOCKER_IMAGE_NAME}" -f docker/D # So make sure we use VERSION_CODE consistently test -z "$VERSION_CODE" && export VERSION_CODE=$VERSION +# PR builds don't populate STABLE_BUILD, but the release builds do, and the build +# bash script requires it, so we need to populate it if it's not present +test -z "$STABLE_BUILD" && export STABLE_BUILD=0 + +# FIXME figure out which of these actually need to be forwarded and which can be eliminated docker run \ --rm \ --security-opt seccomp:unconfined \ @@ -27,6 +32,8 @@ docker run \ -e OAUTH_CLIENT_SECRET \ -e OAUTH_CLIENT_ID \ -e OAUTH_REDIRECT_URI \ + -e SHA7 \ + -e STABLE_BUILD \ -e VERSION_CODE \ "${DOCKER_IMAGE_NAME}" \ sh -c "./build_android.sh" diff --git a/android/docker/Dockerfile b/android/docker/Dockerfile index fe3a83950a..105bcb7cb0 100644 --- a/android/docker/Dockerfile +++ b/android/docker/Dockerfile @@ -73,7 +73,7 @@ RUN mkdir "$HIFI_BASE" && \ RUN git clone https://github.com/jherico/hifi.git && \ cd ~/hifi && \ - git checkout feature/quest_frame_player + git checkout quest/build WORKDIR /home/jenkins/hifi diff --git a/android/gradle.properties b/android/gradle.properties index ac639c5ae7..4236282c8b 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1 +1,2 @@ org.gradle.jvmargs=-Xms2g -Xmx4g +android.debug.obsoleteApi=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 36dba9b2f5..4c0f3c1035 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sat Dec 01 08:32:47 PST 2018 +#Wed Dec 19 13:46:46 PST 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip diff --git a/android/libraries/oculus/build.gradle b/android/libraries/oculus/build.gradle index b072f99eb7..f31efcfe95 100644 --- a/android/libraries/oculus/build.gradle +++ b/android/libraries/oculus/build.gradle @@ -15,3 +15,7 @@ android { targetCompatibility JavaVersion.VERSION_1_8 } } + +dependencies { + implementation project(path: ':qt') +} diff --git a/android/libraries/oculus/src/main/java/io/highfidelity/oculus/OculusMobileActivity.java b/android/libraries/oculus/src/main/java/io/highfidelity/oculus/OculusMobileActivity.java index 01d74ea94d..8ee22749c9 100644 --- a/android/libraries/oculus/src/main/java/io/highfidelity/oculus/OculusMobileActivity.java +++ b/android/libraries/oculus/src/main/java/io/highfidelity/oculus/OculusMobileActivity.java @@ -7,62 +7,72 @@ // package io.highfidelity.oculus; -import android.app.Activity; -import android.content.Intent; import android.os.Bundle; import android.util.Log; import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; -import android.view.WindowManager; + +import org.qtproject.qt5.android.bindings.QtActivity; +import io.highfidelity.utils.HifiUtils; /** * Contains a native surface and forwards the activity lifecycle and surface lifecycle * events to the OculusMobileDisplayPlugin */ -public class OculusMobileActivity extends Activity implements SurfaceHolder.Callback { +public class OculusMobileActivity extends QtActivity implements SurfaceHolder.Callback { private static final String TAG = OculusMobileActivity.class.getSimpleName(); static { System.loadLibrary("oculusMobile"); } + private native void nativeOnCreate(); private native static void nativeOnResume(); private native static void nativeOnPause(); - private native static void nativeOnDestroy(); private native static void nativeOnSurfaceChanged(Surface s); + private native void questNativeOnCreate(); + private native void questNativeOnPause(); + private native void questNativeOnResume(); + private native void questOnAppAfterLoad(); + + private native void questNativeAwayMode(); private SurfaceView mView; private SurfaceHolder mSurfaceHolder; - - public static void launch(Activity activity) { - if (activity != null) { - activity.runOnUiThread(()->{ - activity.startActivity(new Intent(activity, OculusMobileActivity.class)); - }); - } - } - - @Override public void onCreate(Bundle savedInstanceState) { - Log.w(TAG, "QQQ onCreate"); + + if(getIntent().hasExtra("applicationArguments")){ + super.APPLICATION_PARAMETERS=getIntent().getStringExtra("applicationArguments"); + } + super.onCreate(savedInstanceState); - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + + Log.w(TAG, "QQQ onCreate"); // Create a native surface for VR rendering (Qt GL surfaces are not suitable // because of the lack of fine control over the surface callbacks) + // Forward the create message to the JNI code mView = new SurfaceView(this); - setContentView(mView); mView.getHolder().addCallback(this); - // Forward the create message to the JNI code nativeOnCreate(); + questNativeOnCreate(); + } + + public void onAppLoadedComplete() { + Log.w(TAG, "QQQ Load Completed"); + runOnUiThread(() -> { + setContentView(mView); + questOnAppAfterLoad(); + }); } @Override protected void onDestroy() { Log.w(TAG, "QQQ onDestroy"); - if (mSurfaceHolder != null) { - nativeOnSurfaceChanged(null); - } - nativeOnDestroy(); + isPausing=false; + super.onStop(); + nativeOnSurfaceChanged(null); + + Log.w(TAG, "QQQ onDestroy -- SUPER onDestroy"); super.onDestroy(); } @@ -70,33 +80,56 @@ public class OculusMobileActivity extends Activity implements SurfaceHolder.Call protected void onResume() { Log.w(TAG, "QQQ onResume"); super.onResume(); + //Reconnect the global reference back to handler + nativeOnCreate(); + + questNativeOnResume(); nativeOnResume(); + isPausing=false; } @Override protected void onPause() { Log.w(TAG, "QQQ onPause"); - nativeOnPause(); super.onPause(); + + questNativeOnPause(); + nativeOnPause(); + isPausing=true; + } + + @Override + protected void onStop(){ + super.onStop(); + Log.w(TAG, "QQQ_ Onstop called"); + questNativeAwayMode(); + } + + @Override + protected void onRestart() { + super.onRestart(); + Log.w(TAG, "QQQ_ onRestart called"); + questOnAppAfterLoad(); + questNativeAwayMode(); } @Override public void surfaceCreated(SurfaceHolder holder) { - Log.w(TAG, "QQQ surfaceCreated"); + Log.w(TAG, "QQQ_ surfaceCreated"); nativeOnSurfaceChanged(holder.getSurface()); mSurfaceHolder = holder; } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { - Log.w(TAG, "QQQ surfaceChanged"); + Log.w(TAG, "QQQ_ surfaceChanged"); nativeOnSurfaceChanged(holder.getSurface()); mSurfaceHolder = holder; } @Override public void surfaceDestroyed(SurfaceHolder holder) { - Log.w(TAG, "QQQ surfaceDestroyed"); + Log.w(TAG, "QQQ_ surfaceDestroyed"); nativeOnSurfaceChanged(null); mSurfaceHolder = null; } diff --git a/android/libraries/qt/src/main/java/org/qtproject/qt5/android/bindings/QtActivity.java b/android/libraries/qt/src/main/java/org/qtproject/qt5/android/bindings/QtActivity.java index 6a6688ac41..85e93a4267 100644 --- a/android/libraries/qt/src/main/java/org/qtproject/qt5/android/bindings/QtActivity.java +++ b/android/libraries/qt/src/main/java/org/qtproject/qt5/android/bindings/QtActivity.java @@ -70,9 +70,7 @@ public class QtActivity extends Activity { public final String QT_ANDROID_DEFAULT_THEME = QT_ANDROID_THEMES[0]; // sets the default theme. private QtActivityLoader m_loader = new QtActivityLoader(this); - public boolean isLoading; - public boolean keepInterfaceRunning; - + public boolean isPausing=false; public QtActivity() { } @@ -229,10 +227,13 @@ public class QtActivity extends Activity { //--------------------------------------------------------------------------- protected void onCreateHook(Bundle savedInstanceState) { + m_loader.APPLICATION_PARAMETERS = APPLICATION_PARAMETERS; m_loader.ENVIRONMENT_VARIABLES = ENVIRONMENT_VARIABLES; m_loader.QT_ANDROID_THEMES = QT_ANDROID_THEMES; m_loader.QT_ANDROID_DEFAULT_THEME = QT_ANDROID_DEFAULT_THEME; + + m_loader.onCreate(savedInstanceState); } @@ -364,7 +365,10 @@ public class QtActivity extends Activity { @Override protected void onDestroy() { super.onDestroy(); - QtApplication.invokeDelegate(); + + QtNative.terminateQt(); + QtNative.setActivity(null,null); + System.exit(0); } //--------------------------------------------------------------------------- @@ -506,9 +510,9 @@ public class QtActivity extends Activity { super.onPause(); // GC: this trick allow us to show a splash activity until Qt app finishes // loading - if (!isLoading && !keepInterfaceRunning) { - QtApplication.invokeDelegate(); - } + //QtApplication.invokeDelegate(); + + //TODO(Amer): looking into why this messes up pause. } //--------------------------------------------------------------------------- @@ -647,13 +651,13 @@ public class QtActivity extends Activity { @Override protected void onStop() { super.onStop(); - if (!keepInterfaceRunning) { + + if(!isPausing){ QtApplication.invokeDelegate(); } - QtNative.terminateQt(); - QtNative.setActivity(null,null); } + //--------------------------------------------------------------------------- @Override diff --git a/android/settings.gradle b/android/settings.gradle index 699f617cce..c7b70cfde2 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -12,15 +12,26 @@ project(':qt').projectDir = new File(settingsDir, 'libraries/qt') // Applications // -include ':interface' -project(':interface').projectDir = new File(settingsDir, 'apps/interface') +if (!getSettings().hasProperty("SUPPRESS_INTERFACE")) { + include ':interface' + project(':interface').projectDir = new File(settingsDir, 'apps/interface') +} + +if (!getSettings().hasProperty("SUPPRESS_QUEST_INTERFACE")) { + include ':questInterface' + project(':questInterface').projectDir = new File(settingsDir, 'apps/questInterface') +} // // Test projects // -include ':framePlayer' -project(':framePlayer').projectDir = new File(settingsDir, 'apps/framePlayer') +if (!getSettings().hasProperty("SUPPRESS_FRAME_PLAYER")) { + include ':framePlayer' + project(':framePlayer').projectDir = new File(settingsDir, 'apps/framePlayer') +} -include ':questFramePlayer' -project(':questFramePlayer').projectDir = new File(settingsDir, 'apps/questFramePlayer') +if (!getSettings().hasProperty("SUPPRESS_QUEST_FRAME_PLAYER")) { + include ':questFramePlayer' + project(':questFramePlayer').projectDir = new File(settingsDir, 'apps/questFramePlayer') +} diff --git a/assignment-client/src/audio/AudioMixerSlavePool.cpp b/assignment-client/src/audio/AudioMixerSlavePool.cpp index 7cc7ac9f93..78efb98b37 100644 --- a/assignment-client/src/audio/AudioMixerSlavePool.cpp +++ b/assignment-client/src/audio/AudioMixerSlavePool.cpp @@ -64,10 +64,6 @@ bool AudioMixerSlaveThread::try_pop(SharedNodePointer& node) { return _pool._queue.try_pop(node); } -#ifdef AUDIO_SINGLE_THREADED -static AudioMixerSlave slave; -#endif - void AudioMixerSlavePool::processPackets(ConstIter begin, ConstIter end) { _function = &AudioMixerSlave::processPackets; _configure = [](AudioMixerSlave& slave) {}; @@ -87,19 +83,9 @@ void AudioMixerSlavePool::run(ConstIter begin, ConstIter end) { _begin = begin; _end = end; -#ifdef AUDIO_SINGLE_THREADED - _configure(slave); - std::for_each(begin, end, [&](const SharedNodePointer& node) { - _function(slave, node); - }); -#else // fill the queue std::for_each(_begin, _end, [&](const SharedNodePointer& node) { -#if defined(__clang__) && defined(Q_OS_LINUX) _queue.push(node); -#else - _queue.emplace(node); -#endif }); { @@ -119,17 +105,12 @@ void AudioMixerSlavePool::run(ConstIter begin, ConstIter end) { } assert(_queue.empty()); -#endif } void AudioMixerSlavePool::each(std::function functor) { -#ifdef AUDIO_SINGLE_THREADED - functor(slave); -#else for (auto& slave : _slaves) { functor(*slave.get()); } -#endif } void AudioMixerSlavePool::setNumThreads(int numThreads) { @@ -155,9 +136,6 @@ void AudioMixerSlavePool::setNumThreads(int numThreads) { void AudioMixerSlavePool::resize(int numThreads) { assert(_numThreads == (int)_slaves.size()); -#ifdef AUDIO_SINGLE_THREADED - qDebug("%s: running single threaded", __FUNCTION__, numThreads); -#else qDebug("%s: set %d threads (was %d)", __FUNCTION__, numThreads, _numThreads); Lock lock(_mutex); @@ -205,5 +183,4 @@ void AudioMixerSlavePool::resize(int numThreads) { _numThreads = _numStarted = _numFinished = numThreads; assert(_numThreads == (int)_slaves.size()); -#endif } diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 801f28c6f5..e4077d5d46 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -32,6 +33,8 @@ #include #include #include +#include "../AssignmentDynamicFactory.h" +#include "../entities/AssignmentParentFinder.h" const QString AVATAR_MIXER_LOGGING_NAME = "avatar-mixer"; @@ -55,6 +58,9 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) : ThreadedAssignment(message), _slavePool(&_slaveSharedData) { + DependencyManager::registerInheritance(); + DependencyManager::set(); + // make sure we hear about node kills so we can tell the other nodes connect(DependencyManager::get().data(), &NodeList::nodeKilled, this, &AvatarMixer::handleAvatarKilled); @@ -69,6 +75,8 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) : packetReceiver.registerListener(PacketType::RequestsDomainListData, this, "handleRequestsDomainListDataPacket"); packetReceiver.registerListener(PacketType::SetAvatarTraits, this, "queueIncomingPacket"); packetReceiver.registerListener(PacketType::BulkAvatarTraitsAck, this, "queueIncomingPacket"); + packetReceiver.registerListenerForTypes({ PacketType::OctreeStats, PacketType::EntityData, PacketType::EntityErase }, + this, "handleOctreePacket"); packetReceiver.registerListenerForTypes({ PacketType::ReplicatedAvatarIdentity, @@ -240,6 +248,10 @@ void AvatarMixer::start() { int lockWait, nodeTransform, functor; + { + _entityViewer.queryOctree(); + } + // Allow nodes to process any pending/queued packets across our worker threads { auto start = usecTimestampNow(); @@ -252,6 +264,10 @@ void AvatarMixer::start() { }, &lockWait, &nodeTransform, &functor); auto end = usecTimestampNow(); _processQueuedAvatarDataPacketsElapsedTime += (end - start); + + _broadcastAvatarDataLockWait += lockWait; + _broadcastAvatarDataNodeTransform += nodeTransform; + _broadcastAvatarDataNodeFunctor += functor; } // process pending display names... this doesn't currently run on multiple threads, because it @@ -269,6 +285,10 @@ void AvatarMixer::start() { }, &lockWait, &nodeTransform, &functor); auto end = usecTimestampNow(); _displayNameManagementElapsedTime += (end - start); + + _broadcastAvatarDataLockWait += lockWait; + _broadcastAvatarDataNodeTransform += nodeTransform; + _broadcastAvatarDataNodeFunctor += functor; } // this is where we need to put the real work... @@ -691,8 +711,11 @@ void AvatarMixer::handleRadiusIgnoreRequestPacket(QSharedPointer(); nodeList->addSetOfNodeTypesToNodeInterestSet({ - NodeType::Agent, NodeType::EntityScriptServer, + NodeType::Agent, NodeType::EntityScriptServer, NodeType::EntityServer, NodeType::UpstreamAvatarMixer, NodeType::DownstreamAvatarMixer }); // parse the settings to pull out the values we need parseDomainServerSettings(nodeList->getDomainHandler().getSettingsObject()); + setupEntityQuery(); + // start our tight loop... start(); } @@ -939,6 +965,14 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) { qCDebug(avatars) << "Avatar mixer will automatically determine number of threads to use. Using:" << _slavePool.numThreads() << "threads."; } + { + const QString CONNECTION_RATE = "connection_rate"; + auto nodeList = DependencyManager::get(); + auto defaultConnectionRate = nodeList->getMaxConnectionRate(); + int connectionRate = avatarMixerGroupObject[CONNECTION_RATE].toInt((int)defaultConnectionRate); + nodeList->setMaxConnectionRate(connectionRate); + } + const QString AVATARS_SETTINGS_KEY = "avatars"; static const QString MIN_HEIGHT_OPTION = "min_avatar_height"; @@ -976,3 +1010,58 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) { qCDebug(avatars) << "Avatars other than" << _slaveSharedData.skeletonURLWhitelist << "will be replaced by" << (_slaveSharedData.skeletonReplacementURL.isEmpty() ? "default" : _slaveSharedData.skeletonReplacementURL.toString()); } } + +void AvatarMixer::setupEntityQuery() { + _entityViewer.init(); + DependencyManager::registerInheritance(); + DependencyManager::set(_entityViewer.getTree()); + _slaveSharedData.entityTree = _entityViewer.getTree(); + + // ES query: {"avatarPriority": true, "type": "Zone"} + QJsonObject priorityZoneQuery; + priorityZoneQuery["avatarPriority"] = true; + priorityZoneQuery["type"] = "Zone"; + + _entityViewer.getOctreeQuery().setJSONParameters(priorityZoneQuery); +} + +void AvatarMixer::handleOctreePacket(QSharedPointer message, SharedNodePointer senderNode) { + PacketType packetType = message->getType(); + + switch (packetType) { + case PacketType::OctreeStats: + { // Ignore stats, but may have a different Entity packet appended. + OctreeHeadlessViewer::parseOctreeStats(message, senderNode); + const auto piggyBackedSizeWithHeader = message->getBytesLeftToRead(); + if (piggyBackedSizeWithHeader > 0) { + // pull out the piggybacked packet and create a new QSharedPointer for it + auto buffer = std::unique_ptr(new char[piggyBackedSizeWithHeader]); + memcpy(buffer.get(), message->getRawMessage() + message->getPosition(), piggyBackedSizeWithHeader); + + auto newPacket = NLPacket::fromReceivedPacket(std::move(buffer), piggyBackedSizeWithHeader, message->getSenderSockAddr()); + auto newMessage = QSharedPointer::create(*newPacket); + handleOctreePacket(newMessage, senderNode); + } + break; + } + + case PacketType::EntityData: + _entityViewer.processDatagram(*message, senderNode); + break; + + case PacketType::EntityErase: + _entityViewer.processEraseMessage(*message, senderNode); + break; + + default: + qCDebug(avatars) << "Unexpected packet type:" << packetType; + break; + } +} + +void AvatarMixer::aboutToFinish() { + DependencyManager::destroy(); + DependencyManager::destroy(); + + ThreadedAssignment::aboutToFinish(); +} diff --git a/assignment-client/src/avatars/AvatarMixer.h b/assignment-client/src/avatars/AvatarMixer.h index 2992e19b8f..9393ea6c56 100644 --- a/assignment-client/src/avatars/AvatarMixer.h +++ b/assignment-client/src/avatars/AvatarMixer.h @@ -20,6 +20,7 @@ #include #include +#include "../entities/EntityTreeHeadlessViewer.h" #include "AvatarMixerClientData.h" #include "AvatarMixerSlavePool.h" @@ -29,6 +30,7 @@ class AvatarMixer : public ThreadedAssignment { Q_OBJECT public: AvatarMixer(ReceivedMessage& message); + virtual void aboutToFinish() override; static bool shouldReplicateTo(const Node& from, const Node& to) { return to.getType() == NodeType::DownstreamAvatarMixer && @@ -57,6 +59,7 @@ private slots: void handleReplicatedBulkAvatarPacket(QSharedPointer message); void domainSettingsRequestComplete(); void handlePacketVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID); + void handleOctreePacket(QSharedPointer message, SharedNodePointer senderNode); void start(); private: @@ -71,8 +74,13 @@ private: void optionallyReplicatePacket(ReceivedMessage& message, const Node& node); + void setupEntityQuery(); + p_high_resolution_clock::time_point _lastFrameTimestamp; + // Attach to entity tree for avatar-priority zone info. + EntityTreeHeadlessViewer _entityViewer; + // FIXME - new throttling - use these values somehow float _trailingMixRatio { 0.0f }; float _throttlingRatio { 0.0f }; diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index b7d2f5cdf8..cef4383aee 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -16,6 +16,10 @@ #include #include +#include +#include + +#include "AvatarLogging.h" #include "AvatarMixerSlave.h" @@ -62,7 +66,7 @@ int AvatarMixerClientData::processPackets(const SlaveSharedData& slaveSharedData switch (packet->getType()) { case PacketType::AvatarData: - parseData(*packet); + parseData(*packet, slaveSharedData); break; case PacketType::SetAvatarTraits: processSetTraitsMessage(*packet, slaveSharedData, *node); @@ -80,7 +84,42 @@ int AvatarMixerClientData::processPackets(const SlaveSharedData& slaveSharedData return packetsProcessed; } -int AvatarMixerClientData::parseData(ReceivedMessage& message) { +namespace { + using std::static_pointer_cast; + + // Operator to find if a point is within an avatar-priority (hero) Zone Entity. + struct FindPriorityZone { + glm::vec3 position; + bool isInPriorityZone { false }; + float zoneVolume { std::numeric_limits::max() }; + + static bool operation(const OctreeElementPointer& element, void* extraData) { + auto findPriorityZone = static_cast(extraData); + if (element->getAACube().contains(findPriorityZone->position)) { + const EntityTreeElementPointer entityTreeElement = static_pointer_cast(element); + entityTreeElement->forEachEntity([&findPriorityZone](EntityItemPointer item) { + if (item->getType() == EntityTypes::Zone + && item->contains(findPriorityZone->position)) { + auto zoneItem = static_pointer_cast(item); + if (zoneItem->getAvatarPriority() != COMPONENT_MODE_INHERIT) { + float volume = zoneItem->getVolumeEstimate(); + if (volume < findPriorityZone->zoneVolume) { // Smaller volume wins + findPriorityZone->isInPriorityZone = zoneItem->getAvatarPriority() == COMPONENT_MODE_ENABLED; + findPriorityZone->zoneVolume = volume; + } + } + } + }); + return true; // Keep recursing + } else { // Position isn't within this subspace, so end recursion. + return false; + } + } + }; + +} // Close anonymous namespace. + +int AvatarMixerClientData::parseData(ReceivedMessage& message, const SlaveSharedData& slaveSharedData) { // pull the sequence number from the data first uint16_t sequenceNumber; @@ -90,9 +129,33 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message) { incrementNumOutOfOrderSends(); } _lastReceivedSequenceNumber = sequenceNumber; + glm::vec3 oldPosition = getPosition(); // compute the offset to the data payload - return _avatar->parseDataFromBuffer(message.readWithoutCopy(message.getBytesLeftToRead())); + if (!_avatar->parseDataFromBuffer(message.readWithoutCopy(message.getBytesLeftToRead()))) { + return false; + } + + auto newPosition = getPosition(); + if (newPosition != oldPosition) { +//#define AVATAR_HERO_TEST_HACK +#ifdef AVATAR_HERO_TEST_HACK + { + const static QString heroKey { "HERO" }; + _avatar->setPriorityAvatar(_avatar->getDisplayName().contains(heroKey)); + } +#else + EntityTree& entityTree = *slaveSharedData.entityTree; + FindPriorityZone findPriorityZone { newPosition, false } ; + entityTree.recurseTreeWithOperation(&FindPriorityZone::operation, &findPriorityZone); + _avatar->setHasPriority(findPriorityZone.isInPriorityZone); + //if (findPriorityZone.isInPriorityZone) { + // qCWarning(avatars) << "Avatar" << _avatar->getSessionDisplayName() << "in hero zone"; + //} +#endif + } + + return true; } void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message, diff --git a/assignment-client/src/avatars/AvatarMixerClientData.h b/assignment-client/src/avatars/AvatarMixerClientData.h index 843f19cf22..98c8d7e15b 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.h +++ b/assignment-client/src/avatars/AvatarMixerClientData.h @@ -21,7 +21,7 @@ #include #include -#include +#include "MixerAvatar.h" #include #include #include @@ -45,11 +45,12 @@ public: using HRCTime = p_high_resolution_clock::time_point; using PerNodeTraitVersions = std::unordered_map; - int parseData(ReceivedMessage& message) override; - AvatarData& getAvatar() { return *_avatar; } - const AvatarData& getAvatar() const { return *_avatar; } - const AvatarData* getConstAvatarData() const { return _avatar.get(); } - AvatarSharedPointer getAvatarSharedPointer() const { return _avatar; } + using NodeData::parseData; // Avoid clang warning about hiding. + int parseData(ReceivedMessage& message, const SlaveSharedData& SlaveSharedData); + MixerAvatar& getAvatar() { return *_avatar; } + const MixerAvatar& getAvatar() const { return *_avatar; } + const MixerAvatar* getConstAvatarData() const { return _avatar.get(); } + MixerAvatarSharedPointer getAvatarSharedPointer() const { return _avatar; } uint16_t getLastBroadcastSequenceNumber(NLPacket::LocalID nodeID) const; void setLastBroadcastSequenceNumber(NLPacket::LocalID nodeID, uint16_t sequenceNumber) @@ -163,7 +164,7 @@ private: }; PacketQueue _packetQueue; - AvatarSharedPointer _avatar { new AvatarData() }; + MixerAvatarSharedPointer _avatar { new MixerAvatar() }; uint16_t _lastReceivedSequenceNumber { 0 }; std::unordered_map _lastBroadcastSequenceNumbers; diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp index 6b039e2c03..e59c81f4b7 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -281,7 +281,34 @@ AABox computeBubbleBox(const AvatarData& avatar, float bubbleExpansionFactor) { return box; } +namespace { + class SortableAvatar : public PrioritySortUtil::Sortable { + public: + SortableAvatar() = delete; + SortableAvatar(const MixerAvatar* avatar, const Node* avatarNode, uint64_t lastEncodeTime) + : _avatar(avatar), _node(avatarNode), _lastEncodeTime(lastEncodeTime) { + } + glm::vec3 getPosition() const override { return _avatar->getClientGlobalPosition(); } + float getRadius() const override { + glm::vec3 nodeBoxScale = _avatar->getGlobalBoundingBox().getScale(); + return 0.5f * glm::max(nodeBoxScale.x, glm::max(nodeBoxScale.y, nodeBoxScale.z)); + } + uint64_t getTimestamp() const override { + return _lastEncodeTime; + } + const Node* getNode() const { return _node; } + const MixerAvatar* getAvatar() const { return _avatar; } + + private: + const MixerAvatar* _avatar; + const Node* _node; + uint64_t _lastEncodeTime; + }; + +} // Close anonymous namespace. + void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) { + const float AVATAR_HERO_FRACTION { 0.4f }; const Node* destinationNode = node.data(); auto nodeList = DependencyManager::get(); @@ -293,29 +320,30 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) _stats.nodesBroadcastedTo++; - AvatarMixerClientData* nodeData = reinterpret_cast(destinationNode->getLinkedData()); + AvatarMixerClientData* destinationNodeData = reinterpret_cast(destinationNode->getLinkedData()); - nodeData->resetInViewStats(); + destinationNodeData->resetInViewStats(); - const AvatarData& avatar = nodeData->getAvatar(); - glm::vec3 myPosition = avatar.getClientGlobalPosition(); + const AvatarData& avatar = destinationNodeData->getAvatar(); + glm::vec3 destinationPosition = avatar.getClientGlobalPosition(); // reset the internal state for correct random number distribution distribution.reset(); // Estimate number to sort on number sent last frame (with min. of 20). - const int numToSendEst = std::max(int(nodeData->getNumAvatarsSentLastFrame() * 2.5f), 20); + const int numToSendEst = std::max(int(destinationNodeData->getNumAvatarsSentLastFrame() * 2.5f), 20); // reset the number of sent avatars - nodeData->resetNumAvatarsSentLastFrame(); + destinationNodeData->resetNumAvatarsSentLastFrame(); // keep track of outbound data rate specifically for avatar data int numAvatarDataBytes = 0; int identityBytesSent = 0; int traitBytesSent = 0; - // max number of avatarBytes per frame - int maxAvatarBytesPerFrame = int(_maxKbpsPerNode * BYTES_PER_KILOBIT / AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND); + // max number of avatarBytes per frame (13 900, typical) + const int maxAvatarBytesPerFrame = int(_maxKbpsPerNode * BYTES_PER_KILOBIT / AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND); + const int maxHeroBytesPerFrame = int(maxAvatarBytesPerFrame * AVATAR_HERO_FRACTION); // 5555, typical // keep track of the number of other avatars held back in this frame int numAvatarsHeldBack = 0; @@ -325,8 +353,8 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) // When this is true, the AvatarMixer will send Avatar data to a client // about avatars they've ignored or that are out of view - bool PALIsOpen = nodeData->getRequestsDomainListData(); - bool PALWasOpen = nodeData->getPrevRequestsDomainListData(); + bool PALIsOpen = destinationNodeData->getRequestsDomainListData(); + bool PALWasOpen = destinationNodeData->getPrevRequestsDomainListData(); // When this is true, the AvatarMixer will send Avatar data to a client about avatars that have ignored them bool getsAnyIgnored = PALIsOpen && destinationNode->getCanKick(); @@ -337,36 +365,23 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) // compute node bounding box const float MY_AVATAR_BUBBLE_EXPANSION_FACTOR = 4.0f; // magic number determined emperically - AABox nodeBox = computeBubbleBox(avatar, MY_AVATAR_BUBBLE_EXPANSION_FACTOR); - - class SortableAvatar: public PrioritySortUtil::Sortable { - public: - SortableAvatar() = delete; - SortableAvatar(const AvatarData* avatar, const Node* avatarNode, uint64_t lastEncodeTime) - : _avatar(avatar), _node(avatarNode), _lastEncodeTime(lastEncodeTime) {} - glm::vec3 getPosition() const override { return _avatar->getClientGlobalPosition(); } - float getRadius() const override { - glm::vec3 nodeBoxScale = _avatar->getGlobalBoundingBox().getScale(); - return 0.5f * glm::max(nodeBoxScale.x, glm::max(nodeBoxScale.y, nodeBoxScale.z)); - } - uint64_t getTimestamp() const override { - return _lastEncodeTime; - } - const Node* getNode() const { return _node; } - - private: - const AvatarData* _avatar; - const Node* _node; - uint64_t _lastEncodeTime; - }; + AABox destinationNodeBox = computeBubbleBox(avatar, MY_AVATAR_BUBBLE_EXPANSION_FACTOR); // prepare to sort - const auto& cameraViews = nodeData->getViewFrustums(); - PrioritySortUtil::PriorityQueue sortedAvatars(cameraViews, - AvatarData::_avatarSortCoefficientSize, - AvatarData::_avatarSortCoefficientCenter, - AvatarData::_avatarSortCoefficientAge); - sortedAvatars.reserve(_end - _begin); + const auto& cameraViews = destinationNodeData->getViewFrustums(); + + using AvatarPriorityQueue = PrioritySortUtil::PriorityQueue; + // Keep two independent queues, one for heroes and one for the riff-raff. + enum PriorityVariants { kHero, kNonhero }; + AvatarPriorityQueue avatarPriorityQueues[2] = + { + {cameraViews, AvatarData::_avatarSortCoefficientSize, + AvatarData::_avatarSortCoefficientCenter, AvatarData::_avatarSortCoefficientAge}, + {cameraViews, AvatarData::_avatarSortCoefficientSize, + AvatarData::_avatarSortCoefficientCenter, AvatarData::_avatarSortCoefficientAge} + }; + + avatarPriorityQueues[kNonhero].reserve(_end - _begin); for (auto listedNode = _begin; listedNode != _end; ++listedNode) { Node* otherNodeRaw = (*listedNode).data(); @@ -376,47 +391,47 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) continue; } - auto avatarNode = otherNodeRaw; + auto sourceAvatarNode = otherNodeRaw; - bool shouldIgnore = false; + bool sendAvatar = true; // We will consider this source avatar for sending. // We ignore other nodes for a couple of reasons: // 1) ignore bubbles and ignore specific node // 2) the node hasn't really updated it's frame data recently, this can // happen if for example the avatar is connected on a desktop and sending // updates at ~30hz. So every 3 frames we skip a frame. - assert(avatarNode); // we can't have gotten here without the avatarData being a valid key in the map + assert(sourceAvatarNode); // we can't have gotten here without the avatarData being a valid key in the map - const AvatarMixerClientData* avatarClientNodeData = reinterpret_cast(avatarNode->getLinkedData()); - assert(avatarClientNodeData); // we can't have gotten here without avatarNode having valid data + const AvatarMixerClientData* sourceAvatarNodeData = reinterpret_cast(sourceAvatarNode->getLinkedData()); + assert(sourceAvatarNodeData); // we can't have gotten here without sourceAvatarNode having valid data quint64 startIgnoreCalculation = usecTimestampNow(); // make sure we have data for this avatar, that it isn't the same node, // and isn't an avatar that the viewing node has ignored // or that has ignored the viewing node - if ((destinationNode->isIgnoringNodeWithID(avatarNode->getUUID()) && !PALIsOpen) - || (avatarNode->isIgnoringNodeWithID(destinationNode->getUUID()) && !getsAnyIgnored)) { - shouldIgnore = true; + if ((destinationNode->isIgnoringNodeWithID(sourceAvatarNode->getUUID()) && !PALIsOpen) + || (sourceAvatarNode->isIgnoringNodeWithID(destinationNode->getUUID()) && !getsAnyIgnored)) { + sendAvatar = false; } else { // Check to see if the space bubble is enabled // Don't bother with these checks if the other avatar has their bubble enabled and we're gettingAnyIgnored - if (nodeData->isIgnoreRadiusEnabled() || (avatarClientNodeData->isIgnoreRadiusEnabled() && !getsAnyIgnored)) { + if (destinationNodeData->isIgnoreRadiusEnabled() || (sourceAvatarNodeData->isIgnoreRadiusEnabled() && !getsAnyIgnored)) { // Perform the collision check between the two bounding boxes - AABox otherNodeBox = avatarClientNodeData->getAvatar().getDefaultBubbleBox(); - if (nodeBox.touches(otherNodeBox)) { - nodeData->ignoreOther(destinationNode, avatarNode); - shouldIgnore = !getsAnyIgnored; + AABox sourceNodeBox = sourceAvatarNodeData->getAvatar().getDefaultBubbleBox(); + if (destinationNodeBox.touches(sourceNodeBox)) { + destinationNodeData->ignoreOther(destinationNode, sourceAvatarNode); + sendAvatar = getsAnyIgnored; } } // Not close enough to ignore - if (!shouldIgnore) { - nodeData->removeFromRadiusIgnoringSet(avatarNode->getUUID()); + if (sendAvatar) { + destinationNodeData->removeFromRadiusIgnoringSet(sourceAvatarNode->getUUID()); } } - if (!shouldIgnore) { - AvatarDataSequenceNumber lastSeqToReceiver = nodeData->getLastBroadcastSequenceNumber(avatarNode->getLocalID()); - AvatarDataSequenceNumber lastSeqFromSender = avatarClientNodeData->getLastReceivedSequenceNumber(); + if (sendAvatar) { + AvatarDataSequenceNumber lastSeqToReceiver = destinationNodeData->getLastBroadcastSequenceNumber(sourceAvatarNode->getLocalID()); + AvatarDataSequenceNumber lastSeqFromSender = sourceAvatarNodeData->getLastReceivedSequenceNumber(); // FIXME - This code does appear to be working. But it seems brittle. // It supports determining if the frame of data for this "other" @@ -430,26 +445,28 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) // or that somehow we haven't sent if (lastSeqToReceiver == lastSeqFromSender && lastSeqToReceiver != 0) { ++numAvatarsHeldBack; - shouldIgnore = true; + sendAvatar = false; } else if (lastSeqFromSender == 0) { - // We have have not yet recieved any data about this avatar. Ignore it for now + // We have have not yet received any data about this avatar. Ignore it for now // This is important for Agent scripts that are not avatar // so that they don't appear to be an avatar at the origin - shouldIgnore = true; + sendAvatar = false; } else if (lastSeqFromSender - lastSeqToReceiver > 1) { // this is a skip - we still send the packet but capture the presence of the skip so we see it happening ++numAvatarsWithSkippedFrames; } } + quint64 endIgnoreCalculation = usecTimestampNow(); _stats.ignoreCalculationElapsedTime += (endIgnoreCalculation - startIgnoreCalculation); - if (!shouldIgnore) { + if (sendAvatar) { // sort this one for later - const AvatarData* avatarNodeData = avatarClientNodeData->getConstAvatarData(); - auto lastEncodeTime = nodeData->getLastOtherAvatarEncodeTime(avatarNode->getLocalID()); + const MixerAvatar* avatarNodeData = sourceAvatarNodeData->getConstAvatarData(); + auto lastEncodeTime = destinationNodeData->getLastOtherAvatarEncodeTime(sourceAvatarNode->getLocalID()); - sortedAvatars.push(SortableAvatar(avatarNodeData, avatarNode, lastEncodeTime)); + avatarPriorityQueues[avatarNodeData->getHasPriority() ? kHero : kNonhero].push( + SortableAvatar(avatarNodeData, sourceAvatarNode, lastEncodeTime)); } // If Avatar A's PAL WAS open but is no longer open, AND @@ -459,135 +476,153 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) // will be sent when it doesn't need to be (but where it _should_ be OK to send). // However, it's less heavy-handed than using `shouldIgnore`. if (PALWasOpen && !PALIsOpen && - (destinationNode->isIgnoringNodeWithID(avatarNode->getUUID()) || - avatarNode->isIgnoringNodeWithID(destinationNode->getUUID()))) { + (destinationNode->isIgnoringNodeWithID(sourceAvatarNode->getUUID()) || + sourceAvatarNode->isIgnoringNodeWithID(destinationNode->getUUID()))) { // ...send a Kill Packet to Node A, instructing Node A to kill Avatar B, // then have Node A cleanup the killed Node B. auto packet = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID + sizeof(KillAvatarReason), true); - packet->write(avatarNode->getUUID().toRfc4122()); + packet->write(sourceAvatarNode->getUUID().toRfc4122()); packet->writePrimitive(KillAvatarReason::AvatarIgnored); nodeList->sendPacket(std::move(packet), *destinationNode); - nodeData->cleanupKilledNode(avatarNode->getUUID(), avatarNode->getLocalID()); + destinationNodeData->cleanupKilledNode(sourceAvatarNode->getUUID(), sourceAvatarNode->getLocalID()); } - nodeData->setPrevRequestsDomainListData(PALIsOpen); + destinationNodeData->setPrevRequestsDomainListData(PALIsOpen); } // loop through our sorted avatars and allocate our bandwidth to them accordingly - int remainingAvatars = (int)sortedAvatars.size(); + int remainingAvatars = (int)avatarPriorityQueues[kHero].size() + (int)avatarPriorityQueues[kNonhero].size(); auto traitsPacketList = NLPacketList::create(PacketType::BulkAvatarTraits, QByteArray(), true, true); auto avatarPacket = NLPacket::create(PacketType::BulkAvatarData); const int avatarPacketCapacity = avatarPacket->getPayloadCapacity(); int avatarSpaceAvailable = avatarPacketCapacity; int numPacketsSent = 0; + int numAvatarsSent = 0; auto identityPacketList = NLPacketList::create(PacketType::AvatarIdentity, QByteArray(), true, true); - const auto& sortedAvatarVector = sortedAvatars.getSortedVector(numToSendEst); - for (const auto& sortedAvatar : sortedAvatarVector) { - const Node* otherNode = sortedAvatar.getNode(); - auto lastEncodeForOther = sortedAvatar.getTimestamp(); + // Loop over two priorities - hero avatars then everyone else: + for (PriorityVariants currentVariant = kHero; currentVariant <= kNonhero; ++((int&)currentVariant)) { + const auto& sortedAvatarVector = avatarPriorityQueues[currentVariant].getSortedVector(numToSendEst); + for (const auto& sortedAvatar : sortedAvatarVector) { + const Node* sourceNode = sortedAvatar.getNode(); + auto lastEncodeForOther = sortedAvatar.getTimestamp(); - assert(otherNode); // we can't have gotten here without the avatarData being a valid key in the map + assert(sourceNode); // we can't have gotten here without the avatarData being a valid key in the map - AvatarData::AvatarDataDetail detail = AvatarData::NoData; + AvatarData::AvatarDataDetail detail = AvatarData::NoData; - // NOTE: Here's where we determine if we are over budget and drop remaining avatars, - // or send minimal avatar data in uncommon case of PALIsOpen. - int minimRemainingAvatarBytes = minimumBytesPerAvatar * remainingAvatars; - auto frameByteEstimate = identityBytesSent + traitBytesSent + numAvatarDataBytes + minimRemainingAvatarBytes; - bool overBudget = frameByteEstimate > maxAvatarBytesPerFrame; - if (overBudget) { - if (PALIsOpen) { - _stats.overBudgetAvatars++; - detail = AvatarData::PALMinimum; - } else { - _stats.overBudgetAvatars += remainingAvatars; - break; + // NOTE: Here's where we determine if we are over budget and drop remaining avatars, + // or send minimal avatar data in uncommon case of PALIsOpen. + int minimRemainingAvatarBytes = minimumBytesPerAvatar * remainingAvatars; + auto frameByteEstimate = identityBytesSent + traitBytesSent + numAvatarDataBytes + minimRemainingAvatarBytes; + bool overBudget = frameByteEstimate > maxAvatarBytesPerFrame; + if (overBudget) { + if (PALIsOpen) { + _stats.overBudgetAvatars++; + detail = AvatarData::PALMinimum; + } else { + _stats.overBudgetAvatars += remainingAvatars; + break; + } + } + + bool overHeroBudget = currentVariant == kHero && numAvatarDataBytes > maxHeroBytesPerFrame; + if (overHeroBudget) { + break; // No more heroes (this frame). + } + + auto startAvatarDataPacking = chrono::high_resolution_clock::now(); + + const AvatarMixerClientData* sourceNodeData = reinterpret_cast(sourceNode->getLinkedData()); + const MixerAvatar* sourceAvatar = sourceNodeData->getConstAvatarData(); + + // Typically all out-of-view avatars but such avatars' priorities will rise with time: + bool isLowerPriority = currentVariant != kHero && sortedAvatar.getPriority() <= OUT_OF_VIEW_THRESHOLD; // XXX: hero handling? + + if (isLowerPriority) { + detail = PALIsOpen ? AvatarData::PALMinimum : AvatarData::MinimumData; + destinationNodeData->incrementAvatarOutOfView(); + } else if (!overBudget) { + detail = distribution(generator) < AVATAR_SEND_FULL_UPDATE_RATIO ? AvatarData::SendAllData : AvatarData::CullSmallData; + destinationNodeData->incrementAvatarInView(); + + // If the time that the mixer sent AVATAR DATA about Avatar B to Avatar A is BEFORE OR EQUAL TO + // the time that Avatar B flagged an IDENTITY DATA change, send IDENTITY DATA about Avatar B to Avatar A. + if (sourceAvatar->hasProcessedFirstIdentity() + && destinationNodeData->getLastBroadcastTime(sourceNode->getLocalID()) <= sourceNodeData->getIdentityChangeTimestamp()) { + identityBytesSent += sendIdentityPacket(*identityPacketList, sourceNodeData, *destinationNode); + + // remember the last time we sent identity details about this other node to the receiver + destinationNodeData->setLastBroadcastTime(sourceNode->getLocalID(), usecTimestampNow()); + } + } + + QVector& lastSentJointsForOther = destinationNodeData->getLastOtherAvatarSentJoints(sourceNode->getLocalID()); + + const bool distanceAdjust = true; + const bool dropFaceTracking = false; + AvatarDataPacket::SendStatus sendStatus; + sendStatus.sendUUID = true; + + do { + auto startSerialize = chrono::high_resolution_clock::now(); + QByteArray bytes = sourceAvatar->toByteArray(detail, lastEncodeForOther, lastSentJointsForOther, + sendStatus, dropFaceTracking, distanceAdjust, destinationPosition, + &lastSentJointsForOther, avatarSpaceAvailable); + auto endSerialize = chrono::high_resolution_clock::now(); + _stats.toByteArrayElapsedTime += + (quint64)chrono::duration_cast(endSerialize - startSerialize).count(); + + avatarPacket->write(bytes); + avatarSpaceAvailable -= bytes.size(); + numAvatarDataBytes += bytes.size(); + if (!sendStatus || avatarSpaceAvailable < (int)AvatarDataPacket::MIN_BULK_PACKET_SIZE) { + // Weren't able to fit everything. + nodeList->sendPacket(std::move(avatarPacket), *destinationNode); + ++numPacketsSent; + avatarPacket = NLPacket::create(PacketType::BulkAvatarData); + avatarSpaceAvailable = avatarPacketCapacity; + } + } while (!sendStatus); + + if (detail != AvatarData::NoData) { + _stats.numOthersIncluded++; + if (sourceAvatar->getHasPriority()) { + _stats.numHeroesIncluded++; + } + + // increment the number of avatars sent to this receiver + destinationNodeData->incrementNumAvatarsSentLastFrame(); + + // set the last sent sequence number for this sender on the receiver + destinationNodeData->setLastBroadcastSequenceNumber(sourceNode->getLocalID(), + sourceNodeData->getLastReceivedSequenceNumber()); + destinationNodeData->setLastOtherAvatarEncodeTime(sourceNode->getLocalID(), usecTimestampNow()); + } + + auto endAvatarDataPacking = chrono::high_resolution_clock::now(); + _stats.avatarDataPackingElapsedTime += + (quint64)chrono::duration_cast(endAvatarDataPacking - startAvatarDataPacking).count(); + + if (!overBudget) { + // use helper to add any changed traits to our packet list + traitBytesSent += addChangedTraitsToBulkPacket(destinationNodeData, sourceNodeData, *traitsPacketList); + } + numAvatarsSent++; + remainingAvatars--; + } + + if (currentVariant == kHero) { // Dump any remaining heroes into the commoners. + for (auto avIter = sortedAvatarVector.begin() + numAvatarsSent; avIter < sortedAvatarVector.end(); ++avIter) { + avatarPriorityQueues[kNonhero].push(*avIter); } } - - auto startAvatarDataPacking = chrono::high_resolution_clock::now(); - - const AvatarMixerClientData* otherNodeData = reinterpret_cast(otherNode->getLinkedData()); - const AvatarData* otherAvatar = otherNodeData->getConstAvatarData(); - - // Typically all out-of-view avatars but such avatars' priorities will rise with time: - bool isLowerPriority = sortedAvatar.getPriority() <= OUT_OF_VIEW_THRESHOLD; - - if (isLowerPriority) { - detail = PALIsOpen ? AvatarData::PALMinimum : AvatarData::MinimumData; - nodeData->incrementAvatarOutOfView(); - } else if (!overBudget) { - detail = distribution(generator) < AVATAR_SEND_FULL_UPDATE_RATIO ? AvatarData::SendAllData : AvatarData::CullSmallData; - nodeData->incrementAvatarInView(); - - // If the time that the mixer sent AVATAR DATA about Avatar B to Avatar A is BEFORE OR EQUAL TO - // the time that Avatar B flagged an IDENTITY DATA change, send IDENTITY DATA about Avatar B to Avatar A. - if (otherAvatar->hasProcessedFirstIdentity() - && nodeData->getLastBroadcastTime(otherNode->getLocalID()) <= otherNodeData->getIdentityChangeTimestamp()) { - identityBytesSent += sendIdentityPacket(*identityPacketList, otherNodeData, *destinationNode); - - // remember the last time we sent identity details about this other node to the receiver - nodeData->setLastBroadcastTime(otherNode->getLocalID(), usecTimestampNow()); - } - } - - QVector& lastSentJointsForOther = nodeData->getLastOtherAvatarSentJoints(otherNode->getLocalID()); - - const bool distanceAdjust = true; - const bool dropFaceTracking = false; - AvatarDataPacket::SendStatus sendStatus; - sendStatus.sendUUID = true; - - do { - auto startSerialize = chrono::high_resolution_clock::now(); - QByteArray bytes = otherAvatar->toByteArray(detail, lastEncodeForOther, lastSentJointsForOther, - sendStatus, dropFaceTracking, distanceAdjust, myPosition, - &lastSentJointsForOther, avatarSpaceAvailable); - auto endSerialize = chrono::high_resolution_clock::now(); - _stats.toByteArrayElapsedTime += - (quint64)chrono::duration_cast(endSerialize - startSerialize).count(); - - avatarPacket->write(bytes); - avatarSpaceAvailable -= bytes.size(); - numAvatarDataBytes += bytes.size(); - if (!sendStatus || avatarSpaceAvailable < (int)AvatarDataPacket::MIN_BULK_PACKET_SIZE) { - // Weren't able to fit everything. - nodeList->sendPacket(std::move(avatarPacket), *destinationNode); - ++numPacketsSent; - avatarPacket = NLPacket::create(PacketType::BulkAvatarData); - avatarSpaceAvailable = avatarPacketCapacity; - } - } while (!sendStatus); - - if (detail != AvatarData::NoData) { - _stats.numOthersIncluded++; - - // increment the number of avatars sent to this receiver - nodeData->incrementNumAvatarsSentLastFrame(); - - // set the last sent sequence number for this sender on the receiver - nodeData->setLastBroadcastSequenceNumber(otherNode->getLocalID(), - otherNodeData->getLastReceivedSequenceNumber()); - nodeData->setLastOtherAvatarEncodeTime(otherNode->getLocalID(), usecTimestampNow()); - } - - auto endAvatarDataPacking = chrono::high_resolution_clock::now(); - _stats.avatarDataPackingElapsedTime += - (quint64) chrono::duration_cast(endAvatarDataPacking - startAvatarDataPacking).count(); - - if (!overBudget) { - // use helper to add any changed traits to our packet list - traitBytesSent += addChangedTraitsToBulkPacket(nodeData, otherNodeData, *traitsPacketList); - } - - remainingAvatars--; } - if (nodeData->getNumAvatarsSentLastFrame() > numToSendEst) { - qCWarning(avatars) << "More avatars sent than upper estimate" << nodeData->getNumAvatarsSentLastFrame() + if (destinationNodeData->getNumAvatarsSentLastFrame() > numToSendEst) { + qCWarning(avatars) << "More avatars sent than upper estimate" << destinationNodeData->getNumAvatarsSentLastFrame() << " / " << numToSendEst; } @@ -618,12 +653,12 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) } // record the bytes sent for other avatar data in the AvatarMixerClientData - nodeData->recordSentAvatarData(numAvatarDataBytes, traitBytesSent); + destinationNodeData->recordSentAvatarData(numAvatarDataBytes, traitBytesSent); // record the number of avatars held back this frame - nodeData->recordNumOtherAvatarStarves(numAvatarsHeldBack); - nodeData->recordNumOtherAvatarSkips(numAvatarsWithSkippedFrames); + destinationNodeData->recordNumOtherAvatarStarves(numAvatarsHeldBack); + destinationNodeData->recordNumOtherAvatarSkips(numAvatarsWithSkippedFrames); quint64 endPacketSending = usecTimestampNow(); _stats.packetSendingElapsedTime += (endPacketSending - startPacketSending); diff --git a/assignment-client/src/avatars/AvatarMixerSlave.h b/assignment-client/src/avatars/AvatarMixerSlave.h index 91bb02fd55..8c5ad6b181 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.h +++ b/assignment-client/src/avatars/AvatarMixerSlave.h @@ -32,6 +32,7 @@ public: int numIdentityPacketsSent { 0 }; int numOthersIncluded { 0 }; int overBudgetAvatars { 0 }; + int numHeroesIncluded { 0 }; quint64 ignoreCalculationElapsedTime { 0 }; quint64 avatarDataPackingElapsedTime { 0 }; @@ -57,6 +58,7 @@ public: numIdentityPacketsSent = 0; numOthersIncluded = 0; overBudgetAvatars = 0; + numHeroesIncluded = 0; ignoreCalculationElapsedTime = 0; avatarDataPackingElapsedTime = 0; @@ -80,6 +82,7 @@ public: numIdentityPacketsSent += rhs.numIdentityPacketsSent; numOthersIncluded += rhs.numOthersIncluded; overBudgetAvatars += rhs.overBudgetAvatars; + numHeroesIncluded += rhs.numHeroesIncluded; ignoreCalculationElapsedTime += rhs.ignoreCalculationElapsedTime; avatarDataPackingElapsedTime += rhs.avatarDataPackingElapsedTime; @@ -90,9 +93,13 @@ public: } }; +class EntityTree; +using EntityTreePointer = std::shared_ptr; + struct SlaveSharedData { QStringList skeletonURLWhitelist; QUrl skeletonReplacementURL; + EntityTreePointer entityTree; }; class AvatarMixerSlave { diff --git a/assignment-client/src/avatars/AvatarMixerSlavePool.cpp b/assignment-client/src/avatars/AvatarMixerSlavePool.cpp index cf842ac792..013d914cbe 100644 --- a/assignment-client/src/avatars/AvatarMixerSlavePool.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlavePool.cpp @@ -63,10 +63,6 @@ bool AvatarMixerSlaveThread::try_pop(SharedNodePointer& node) { return _pool._queue.try_pop(node); } -#ifdef AVATAR_SINGLE_THREADED -static AvatarMixerSlave slave; -#endif - void AvatarMixerSlavePool::processIncomingPackets(ConstIter begin, ConstIter end) { _function = &AvatarMixerSlave::processIncomingPackets; _configure = [=](AvatarMixerSlave& slave) { @@ -89,19 +85,9 @@ void AvatarMixerSlavePool::run(ConstIter begin, ConstIter end) { _begin = begin; _end = end; -#ifdef AUDIO_SINGLE_THREADED - _configure(slave); - std::for_each(begin, end, [&](const SharedNodePointer& node) { - _function(slave, node); -}); -#else // fill the queue std::for_each(_begin, _end, [&](const SharedNodePointer& node) { -#if defined(__clang__) && defined(Q_OS_LINUX) _queue.push(node); -#else - _queue.emplace(node); -#endif }); { @@ -121,18 +107,13 @@ void AvatarMixerSlavePool::run(ConstIter begin, ConstIter end) { } assert(_queue.empty()); -#endif } void AvatarMixerSlavePool::each(std::function functor) { -#ifdef AVATAR_SINGLE_THREADED - functor(slave); -#else for (auto& slave : _slaves) { functor(*slave.get()); } -#endif } void AvatarMixerSlavePool::setNumThreads(int numThreads) { @@ -158,9 +139,6 @@ void AvatarMixerSlavePool::setNumThreads(int numThreads) { void AvatarMixerSlavePool::resize(int numThreads) { assert(_numThreads == (int)_slaves.size()); -#ifdef AVATAR_SINGLE_THREADED - qDebug("%s: running single threaded", __FUNCTION__, numThreads); -#else qDebug("%s: set %d threads (was %d)", __FUNCTION__, numThreads, _numThreads); Lock lock(_mutex); @@ -208,5 +186,4 @@ void AvatarMixerSlavePool::resize(int numThreads) { _numThreads = _numStarted = _numFinished = numThreads; assert(_numThreads == (int)_slaves.size()); -#endif } diff --git a/assignment-client/src/avatars/MixerAvatar.h b/assignment-client/src/avatars/MixerAvatar.h new file mode 100644 index 0000000000..4c3ded4582 --- /dev/null +++ b/assignment-client/src/avatars/MixerAvatar.h @@ -0,0 +1,31 @@ +// +// MixerAvatar.h +// assignment-client/src/avatars +// +// Created by Simon Walton Feb 2019. +// Copyright 2019 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 +// + +// Avatar class for use within the avatar mixer - encapsulates data required only for +// sorting priorities within the mixer. + +#ifndef hifi_MixerAvatar_h +#define hifi_MixerAvatar_h + +#include + +class MixerAvatar : public AvatarData { +public: + bool getHasPriority() const { return _hasPriority; } + void setHasPriority(bool hasPriority) { _hasPriority = hasPriority; } + +private: + bool _hasPriority { false }; +}; + +using MixerAvatarSharedPointer = std::shared_ptr; + +#endif // hifi_MixerAvatar_h diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index e993bea358..477d3dd612 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -1203,7 +1203,8 @@ void OctreeServer::beginRunning() { auto nodeList = DependencyManager::get(); // we need to ask the DS about agents so we can ping/reply with them - nodeList->addSetOfNodeTypesToNodeInterestSet({ NodeType::Agent, NodeType::EntityScriptServer }); + nodeList->addSetOfNodeTypesToNodeInterestSet({ NodeType::Agent, NodeType::EntityScriptServer, + NodeType::AvatarMixer }); beforeRun(); // after payload has been processed diff --git a/cmake/macros/AutoScribeShader.cmake b/cmake/macros/AutoScribeShader.cmake index 64fbcc4ea6..c5bb2b4054 100755 --- a/cmake/macros/AutoScribeShader.cmake +++ b/cmake/macros/AutoScribeShader.cmake @@ -270,6 +270,16 @@ macro(AUTOSCRIBE_SHADER_LIBS) set(AUTOSCRIBE_SHADERGEN_COMMANDS_FILE ${CMAKE_CURRENT_BINARY_DIR}/shadergen.txt) file(WRITE ${AUTOSCRIBE_SHADERGEN_COMMANDS_FILE} "${AUTOSCRIBE_SHADERGEN_COMMANDS}") + if (HIFI_ANDROID) + if ( + (${HIFI_ANDROID_APP} STREQUAL "questInterface") OR + (${HIFI_ANDROID_APP} STREQUAL "questFramePlayer") OR + (${HIFI_ANDROID_APP} STREQUAL "framePlayer") + ) + set(EXTRA_SHADERGEN_ARGS --extensions EXT_clip_cull_distance) + endif() + endif() + # A custom python script which will generate all our shader artifacts add_custom_command( OUTPUT ${SCRIBED_SHADERS} ${SPIRV_SHADERS} ${REFLECTED_SHADERS} @@ -279,6 +289,7 @@ macro(AUTOSCRIBE_SHADER_LIBS) --tools-dir ${VCPKG_TOOLS_DIR} --build-dir ${CMAKE_CURRENT_BINARY_DIR} --source-dir ${CMAKE_SOURCE_DIR} + ${EXTRA_SHADERGEN_ARGS} DEPENDS ${AUTOSCRIBE_SHADER_HEADERS} ${CMAKE_SOURCE_DIR}/tools/shadergen.py ${ALL_SCRIBE_SHADERS}) add_custom_target(shadergen DEPENDS ${SCRIBED_SHADERS} ${SPIRV_SHADERS} ${REFLECTED_SHADERS}) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 49023c9af8..140c7d6c17 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -1302,6 +1302,14 @@ "placeholder": "1", "default": "1", "advanced": true + }, + { + "name": "connection_rate", + "label": "Connection Rate", + "help": "Number of new agents that can connect to the mixer every second", + "placeholder": "50", + "default": "50", + "advanced": true } ] }, diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 258038b8f1..8d5cb165cb 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1243,12 +1243,11 @@ void DomainServer::broadcastNewNode(const SharedNodePointer& addedNode) { limitedNodeList->eachMatchingNode( [this, addedNode](const SharedNodePointer& node)->bool { - if (node->getLinkedData() && node->getActiveSocket() && node != addedNode) { - // is the added Node in this node's interest list? - return isInInterestSet(node, addedNode); - } else { - return false; - } + // is the added Node in this node's interest list? + return node->getLinkedData() + && node->getActiveSocket() + && node != addedNode + && isInInterestSet(node, addedNode); }, [this, &addNodePacket, connectionSecretIndex, addedNode, limitedNodeListWeak](const SharedNodePointer& node) { // send off this packet to the node @@ -2548,7 +2547,7 @@ bool DomainServer::processPendingContent(HTTPConnection* connection, QString ite _pendingFileContent.seek(_pendingFileContent.size()); _pendingFileContent.write(dataChunk); _pendingFileContent.close(); - + // Respond immediately - will timeout if we wait for restore. connection->respond(HTTPConnection::StatusCode200); if (itemName == "restore-file" || itemName == "restore-file-chunk-final" || itemName == "restore-file-chunk-only") { diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 780fad15f2..4e833f6b77 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -25,13 +25,13 @@ #include #include +#include #include #include #include #include #include #include -#include //for KillAvatarReason #include #include "DomainServerNodeData.h" @@ -870,14 +870,6 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointerwrite(nodeUUID.toRfc4122()); - packet->writePrimitive(KillAvatarReason::NoReason); - - // send to avatar mixer, it sends the kill to everyone else - limitedNodeList->broadcastToNodes(std::move(packet), NodeSet() << NodeType::AvatarMixer); - if (newPermissions) { qDebug() << "Removing connect permission for node" << uuidStringWithoutCurlyBraces(matchingNode->getUUID()) << "after kick request from" << uuidStringWithoutCurlyBraces(sendingNode->getUUID()); diff --git a/hifi_android.py b/hifi_android.py index 2e6a42d127..b8a606a82f 100644 --- a/hifi_android.py +++ b/hifi_android.py @@ -53,9 +53,9 @@ ANDROID_PACKAGES = { 'includeLibs': ['libvrapi.so'] }, 'oculusPlatform': { - 'file': 'OVRPlatformSDK_v1.32.0.zip', - 'versionId': 'jG9DB16zOGxSrmtZy4jcQnwO0TJUuaeL', - 'checksum': 'ab5b203b3a39a56ab148d68fff769e05', + 'file': 'OVRPlatformSDK_v1.34.0.zip', + 'versionId': 'vbRUkkyzUAXfTGSEtuiUr_7.Fm5h5BZk', + 'checksum': '16e4c5f39520f122bc49cb6d5bb88289', 'sharedLibFolder': 'Android/libs/arm64-v8a', 'includeLibs': ['libovrplatformloader.so'] }, diff --git a/interface/resources/avatar/animations/run_fwd.fbx b/interface/resources/avatar/animations/run_fwd.fbx index 2ea035e694..86add969e5 100644 Binary files a/interface/resources/avatar/animations/run_fwd.fbx and b/interface/resources/avatar/animations/run_fwd.fbx differ diff --git a/interface/resources/avatar/avatar-animation.json b/interface/resources/avatar/avatar-animation.json index 50fe5019f9..27e45daa7b 100644 --- a/interface/resources/avatar/avatar-animation.json +++ b/interface/resources/avatar/avatar-animation.json @@ -1016,8 +1016,8 @@ "type": "clip", "data": { "url": "qrc:///avatar/animations/run_fwd.fbx", - "startFrame": 0.0, - "endFrame": 21.0, + "startFrame": 1.0, + "endFrame": 22.0, "timeScale": 1.0, "loopFlag": true }, diff --git a/interface/resources/avatar/avatar-animation_withSplineIKNode.json b/interface/resources/avatar/avatar-animation_withSplineIKNode.json new file mode 100644 index 0000000000..b1f198c52c --- /dev/null +++ b/interface/resources/avatar/avatar-animation_withSplineIKNode.json @@ -0,0 +1,2229 @@ +{ + "version": "1.1", + "root": { + "id": "userAnimStateMachine", + "type": "stateMachine", + "data": { + "currentState": "userAnimNone", + "states": [ + { + "id": "userAnimNone", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { + "var": "userAnimA", + "state": "userAnimA" + }, + { + "var": "userAnimB", + "state": "userAnimB" + } + ] + }, + { + "id": "userAnimA", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { + "var": "userAnimNone", + "state": "userAnimNone" + }, + { + "var": "userAnimB", + "state": "userAnimB" + } + ] + }, + { + "id": "userAnimB", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { + "var": "userAnimNone", + "state": "userAnimNone" + }, + { + "var": "userAnimA", + "state": "userAnimA" + } + ] + } + ] + }, + "children": [ + { + "id": "userAnimNone", + "type": "poleVectorConstraint", + "data": { + "enabled": false, + "referenceVector": [ 0, 0, 1 ], + "baseJointName": "RightUpLeg", + "midJointName": "RightLeg", + "tipJointName": "RightFoot", + "enabledVar": "rightFootPoleVectorEnabled", + "poleVectorVar": "rightFootPoleVector" + }, + "children": [ + { + "id": "rightFootIK", + "type": "twoBoneIK", + "data": { + "alpha": 1.0, + "enabled": false, + "interpDuration": 15, + "baseJointName": "RightUpLeg", + "midJointName": "RightLeg", + "tipJointName": "RightFoot", + "midHingeAxis": [ -1, 0, 0 ], + "alphaVar": "rightFootIKAlpha", + "enabledVar": "rightFootIKEnabled", + "endEffectorRotationVarVar": "rightFootIKRotationVar", + "endEffectorPositionVarVar": "rightFootIKPositionVar" + }, + "children": [ + { + "id": "leftFootPoleVector", + "type": "poleVectorConstraint", + "data": { + "enabled": false, + "referenceVector": [ 0, 0, 1 ], + "baseJointName": "LeftUpLeg", + "midJointName": "LeftLeg", + "tipJointName": "LeftFoot", + "enabledVar": "leftFootPoleVectorEnabled", + "poleVectorVar": "leftFootPoleVector" + }, + "children": [ + { + "id": "leftFootIK", + "type": "twoBoneIK", + "data": { + "alpha": 1.0, + "enabled": false, + "interpDuration": 15, + "baseJointName": "LeftUpLeg", + "midJointName": "LeftLeg", + "tipJointName": "LeftFoot", + "midHingeAxis": [ -1, 0, 0 ], + "alphaVar": "leftFootIKAlpha", + "enabledVar": "leftFootIKEnabled", + "endEffectorRotationVarVar": "leftFootIKRotationVar", + "endEffectorPositionVarVar": "leftFootIKPositionVar" + }, + "children": [ + { + "id": "rightHandPoleVector", + "type": "poleVectorConstraint", + "data": { + "enabled": false, + "referenceVector": [ -1, 0, 0 ], + "baseJointName": "RightArm", + "midJointName": "RightForeArm", + "tipJointName": "RightHand", + "enabledVar": "rightHandPoleVectorEnabled", + "poleVectorVar": "rightHandPoleVector" + }, + "children": [ + { + "id": "rightHandIK", + "type": "twoBoneIK", + "data": { + "alpha": 1.0, + "enabled": false, + "interpDuration": 15, + "baseJointName": "RightArm", + "midJointName": "RightForeArm", + "tipJointName": "RightHand", + "midHingeAxis": [ 0, 0, -1 ], + "alphaVar": "rightHandIKAlpha", + "enabledVar": "rightHandIKEnabled", + "endEffectorRotationVarVar": "rightHandIKRotationVar", + "endEffectorPositionVarVar": "rightHandIKPositionVar" + }, + "children": [ + { + "id": "leftHandPoleVector", + "type": "poleVectorConstraint", + "data": { + "enabled": false, + "referenceVector": [ 1, 0, 0 ], + "baseJointName": "LeftArm", + "midJointName": "LeftForeArm", + "tipJointName": "LeftHand", + "enabledVar": "leftHandPoleVectorEnabled", + "poleVectorVar": "leftHandPoleVector" + }, + "children": [ + { + "id": "leftHandIK", + "type": "twoBoneIK", + "data": { + "alpha": 1.0, + "enabled": false, + "interpDuration": 15, + "baseJointName": "LeftArm", + "midJointName": "LeftForeArm", + "tipJointName": "LeftHand", + "midHingeAxis": [ 0, 0, 1 ], + "alphaVar": "leftHandIKAlpha", + "enabledVar": "leftHandIKEnabled", + "endEffectorRotationVarVar": "leftHandIKRotationVar", + "endEffectorPositionVarVar": "leftHandIKPositionVar" + }, + "children": [ + { + "id": "userSplineIK", + "type": "splineIK", + "data": { + "alpha": 1.0, + "enabled": false, + "interpDuration": 15, + "baseJointName": "Hips", + "midJointName": "Spine2", + "tipJointName": "Head", + "basePositionVar": "hipsPosition", + "baseRotationVar": "hipsRotation", + "midPositionVar": "spine2Position", + "midRotationVar": "spine2Rotation", + "tipPositionVar": "headPosition", + "tipRotationVar": "headRotation", + "alphaVar": "splineIKAlpha", + "enabledVar": "splineIKEnabled", + "tipTargetFlexCoefficients": [ 1.0, 1.0, 1.0, 1.0, 1.0 ], + "midTargetFlexCoefficients": [ 1.0, 1.0, 1.0 ] + }, + "children": [ + { + "id": "defaultPoseOverlay", + "type": "overlay", + "data": { + "alpha": 0.0, + "alphaVar": "defaultPoseOverlayAlpha", + "boneSet": "fullBody", + "boneSetVar": "defaultPoseOverlayBoneSet" + }, + "children": [ + { + "id": "defaultPose", + "type": "defaultPose", + "data": { + }, + "children": [] + }, + { + "id": "rightHandOverlay", + "type": "overlay", + "data": { + "alpha": 0.0, + "boneSet": "rightHand", + "alphaVar": "rightHandOverlayAlpha" + }, + "children": [ + { + "id": "rightHandStateMachine", + "type": "stateMachine", + "data": { + "currentState": "rightHandGrasp", + "states": [ + { + "id": "rightHandGrasp", + "interpTarget": 3, + "interpDuration": 3, + "transitions": [ + { + "var": "isRightIndexPoint", + "state": "rightIndexPoint" + }, + { + "var": "isRightThumbRaise", + "state": "rightThumbRaise" + }, + { + "var": "isRightIndexPointAndThumbRaise", + "state": "rightIndexPointAndThumbRaise" + } + ] + }, + { + "id": "rightIndexPoint", + "interpTarget": 15, + "interpDuration": 3, + "transitions": [ + { + "var": "isRightHandGrasp", + "state": "rightHandGrasp" + }, + { + "var": "isRightThumbRaise", + "state": "rightThumbRaise" + }, + { + "var": "isRightIndexPointAndThumbRaise", + "state": "rightIndexPointAndThumbRaise" + } + ] + }, + { + "id": "rightThumbRaise", + "interpTarget": 15, + "interpDuration": 3, + "transitions": [ + { + "var": "isRightHandGrasp", + "state": "rightHandGrasp" + }, + { + "var": "isRightIndexPoint", + "state": "rightIndexPoint" + }, + { + "var": "isRightIndexPointAndThumbRaise", + "state": "rightIndexPointAndThumbRaise" + } + ] + }, + { + "id": "rightIndexPointAndThumbRaise", + "interpTarget": 15, + "interpDuration": 3, + "transitions": [ + { + "var": "isRightHandGrasp", + "state": "rightHandGrasp" + }, + { + "var": "isRightIndexPoint", + "state": "rightIndexPoint" + }, + { + "var": "isRightThumbRaise", + "state": "rightThumbRaise" + } + ] + } + ] + }, + "children": [ + { + "id": "rightHandGrasp", + "type": "blendLinear", + "data": { + "alpha": 0.0, + "alphaVar": "rightHandGraspAlpha" + }, + "children": [ + { + "id": "rightHandGraspOpen", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/hydra_pose_open_right.fbx", + "startFrame": 0.0, + "endFrame": 0.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "rightHandGraspClosed", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/hydra_pose_closed_right.fbx", + "startFrame": 0.0, + "endFrame": 0.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + }, + { + "id": "rightIndexPoint", + "type": "blendLinear", + "data": { + "alpha": 0.0, + "alphaVar": "rightHandGraspAlpha" + }, + "children": [ + { + "id": "rightIndexPointOpen", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/touch_point_open_right.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "rightIndexPointClosed", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/touch_point_closed_right.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + }, + { + "id": "rightThumbRaise", + "type": "blendLinear", + "data": { + "alpha": 0.0, + "alphaVar": "rightHandGraspAlpha" + }, + "children": [ + { + "id": "rightThumbRaiseOpen", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/touch_thumb_open_right.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "rightThumbRaiseClosed", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/touch_thumb_closed_right.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + }, + { + "id": "rightIndexPointAndThumbRaise", + "type": "blendLinear", + "data": { + "alpha": 0.0, + "alphaVar": "rightHandGraspAlpha" + }, + "children": [ + { + "id": "rightIndexPointAndThumbRaiseOpen", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/touch_thumb_point_open_right.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "rightIndexPointAndThumbRaiseClosed", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/touch_thumb_point_closed_right.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + } + ] + }, + { + "id": "leftHandOverlay", + "type": "overlay", + "data": { + "alpha": 0.0, + "boneSet": "leftHand", + "alphaVar": "leftHandOverlayAlpha" + }, + "children": [ + { + "id": "leftHandStateMachine", + "type": "stateMachine", + "data": { + "currentState": "leftHandGrasp", + "states": [ + { + "id": "leftHandGrasp", + "interpTarget": 3, + "interpDuration": 3, + "transitions": [ + { + "var": "isLeftIndexPoint", + "state": "leftIndexPoint" + }, + { + "var": "isLeftThumbRaise", + "state": "leftThumbRaise" + }, + { + "var": "isLeftIndexPointAndThumbRaise", + "state": "leftIndexPointAndThumbRaise" + } + ] + }, + { + "id": "leftIndexPoint", + "interpTarget": 15, + "interpDuration": 3, + "transitions": [ + { + "var": "isLeftHandGrasp", + "state": "leftHandGrasp" + }, + { + "var": "isLeftThumbRaise", + "state": "leftThumbRaise" + }, + { + "var": "isLeftIndexPointAndThumbRaise", + "state": "leftIndexPointAndThumbRaise" + } + ] + }, + { + "id": "leftThumbRaise", + "interpTarget": 15, + "interpDuration": 3, + "transitions": [ + { + "var": "isLeftHandGrasp", + "state": "leftHandGrasp" + }, + { + "var": "isLeftIndexPoint", + "state": "leftIndexPoint" + }, + { + "var": "isLeftIndexPointAndThumbRaise", + "state": "leftIndexPointAndThumbRaise" + } + ] + }, + { + "id": "leftIndexPointAndThumbRaise", + "interpTarget": 15, + "interpDuration": 3, + "transitions": [ + { + "var": "isLeftHandGrasp", + "state": "leftHandGrasp" + }, + { + "var": "isLeftIndexPoint", + "state": "leftIndexPoint" + }, + { + "var": "isLeftThumbRaise", + "state": "leftThumbRaise" + } + ] + } + ] + }, + "children": [ + { + "id": "leftHandGrasp", + "type": "blendLinear", + "data": { + "alpha": 0.0, + "alphaVar": "leftHandGraspAlpha" + }, + "children": [ + { + "id": "leftHandGraspOpen", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/hydra_pose_open_left.fbx", + "startFrame": 0.0, + "endFrame": 0.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "leftHandGraspClosed", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/hydra_pose_closed_left.fbx", + "startFrame": 10.0, + "endFrame": 10.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + }, + { + "id": "leftIndexPoint", + "type": "blendLinear", + "data": { + "alpha": 0.0, + "alphaVar": "leftHandGraspAlpha" + }, + "children": [ + { + "id": "leftIndexPointOpen", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/touch_point_open_left.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "leftIndexPointClosed", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/touch_point_closed_left.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + }, + { + "id": "leftThumbRaise", + "type": "blendLinear", + "data": { + "alpha": 0.0, + "alphaVar": "leftHandGraspAlpha" + }, + "children": [ + { + "id": "leftThumbRaiseOpen", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/touch_thumb_open_left.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "leftThumbRaiseClosed", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/touch_thumb_closed_left.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + }, + { + "id": "leftIndexPointAndThumbRaise", + "type": "blendLinear", + "data": { + "alpha": 0.0, + "alphaVar": "leftHandGraspAlpha" + }, + "children": [ + { + "id": "leftIndexPointAndThumbRaiseOpen", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/touch_thumb_point_open_left.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "leftIndexPointAndThumbRaiseClosed", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/touch_thumb_point_closed_left.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + } + ] + }, + { + "id": "mainStateMachine", + "type": "stateMachine", + "data": { + "outputJoints": [ "LeftFoot", "RightFoot" ], + "currentState": "idle", + "states": [ + { + "id": "idle", + "interpTarget": 20, + "interpDuration": 8, + "interpType": "snapshotPrev", + "transitions": [ + { + "var": "isMovingForward", + "state": "WALKFWD" + }, + { + "var": "isMovingBackward", + "state": "WALKBWD" + }, + { + "var": "isMovingRight", + "state": "STRAFERIGHT" + }, + { + "var": "isMovingLeft", + "state": "STRAFELEFT" + }, + { + "var": "isTurningRight", + "state": "turnRight" + }, + { + "var": "isTurningLeft", + "state": "turnLeft" + }, + { + "var": "isFlying", + "state": "fly" + }, + { + "var": "isTakeoffStand", + "state": "takeoffStand" + }, + { + "var": "isTakeoffRun", + "state": "TAKEOFFRUN" + }, + { + "var": "isInAirStand", + "state": "inAirStand" + }, + { + "var": "isInAirRun", + "state": "INAIRRUN" + }, + { + "var": "isMovingRightHmd", + "state": "strafeRightHmd" + }, + { + "var": "isMovingLeftHmd", + "state": "strafeLeftHmd" + } + ] + }, + { + "id": "idleToWalkFwd", + "interpTarget": 12, + "interpDuration": 8, + "transitions": [ + { + "var": "idleToWalkFwdOnDone", + "state": "WALKFWD" + }, + { + "var": "isNotMoving", + "state": "idle" + }, + { + "var": "isMovingBackward", + "state": "WALKBWD" + }, + { + "var": "isMovingRight", + "state": "STRAFERIGHT" + }, + { + "var": "isMovingLeft", + "state": "STRAFELEFT" + }, + { + "var": "isTurningRight", + "state": "turnRight" + }, + { + "var": "isTurningLeft", + "state": "turnLeft" + }, + { + "var": "isFlying", + "state": "fly" + }, + { + "var": "isTakeoffStand", + "state": "takeoffStand" + }, + { + "var": "isTakeoffRun", + "state": "TAKEOFFRUN" + }, + { + "var": "isInAirStand", + "state": "inAirStand" + }, + { + "var": "isInAirRun", + "state": "INAIRRUN" + }, + { + "var": "isMovingRightHmd", + "state": "strafeRightHmd" + }, + { + "var": "isMovingLeftHmd", + "state": "strafeLeftHmd" + } + ] + }, + { + "id": "idleSettle", + "interpTarget": 15, + "interpDuration": 8, + "interpType": "snapshotPrev", + "transitions": [ + { + "var": "idleSettleOnDone", + "state": "idle" + }, + { + "var": "isMovingForward", + "state": "WALKFWD" + }, + { + "var": "isMovingBackward", + "state": "WALKBWD" + }, + { + "var": "isMovingRight", + "state": "STRAFERIGHT" + }, + { + "var": "isMovingLeft", + "state": "STRAFELEFT" + }, + { + "var": "isMovingRightHmd", + "state": "strafeRightHmd" + }, + { + "var": "isMovingLeftHmd", + "state": "strafeLeftHmd" + }, + { + "var": "isTurningRight", + "state": "turnRight" + }, + { + "var": "isTurningLeft", + "state": "turnLeft" + }, + { + "var": "isFlying", + "state": "fly" + }, + { + "var": "isTakeoffStand", + "state": "takeoffStand" + }, + { + "var": "isTakeoffRun", + "state": "TAKEOFFRUN" + }, + { + "var": "isInAirStand", + "state": "inAirStand" + }, + { + "var": "isInAirRun", + "state": "INAIRRUN" + } + ] + }, + { + "id": "WALKFWD", + "interpTarget": 35, + "interpDuration": 10, + "interpType": "snapshotPrev", + "transitions": [ + { + "var": "isNotMoving", + "state": "idleSettle" + }, + { + "var": "isMovingBackward", + "state": "WALKBWD" + }, + { + "var": "isMovingRight", + "state": "STRAFERIGHT" + }, + { + "var": "isMovingLeft", + "state": "STRAFELEFT" + }, + { + "var": "isTurningRight", + "state": "turnRight" + }, + { + "var": "isTurningLeft", + "state": "turnLeft" + }, + { + "var": "isFlying", + "state": "fly" + }, + { + "var": "isTakeoffStand", + "state": "takeoffStand" + }, + { + "var": "isTakeoffRun", + "state": "TAKEOFFRUN" + }, + { + "var": "isInAirStand", + "state": "inAirStand" + }, + { + "var": "isInAirRun", + "state": "INAIRRUN" + }, + { + "var": "isMovingRightHmd", + "state": "strafeRightHmd" + }, + { + "var": "isMovingLeftHmd", + "state": "strafeLeftHmd" + } + ] + }, + { + "id": "WALKBWD", + "interpTarget": 35, + "interpDuration": 10, + "interpType": "snapshotPrev", + "transitions": [ + { + "var": "isNotMoving", + "state": "idleSettle" + }, + { + "var": "isMovingForward", + "state": "WALKFWD" + }, + { + "var": "isMovingRight", + "state": "STRAFERIGHT" + }, + { + "var": "isMovingLeft", + "state": "STRAFELEFT" + }, + { + "var": "isTurningRight", + "state": "turnRight" + }, + { + "var": "isTurningLeft", + "state": "turnLeft" + }, + { + "var": "isFlying", + "state": "fly" + }, + { + "var": "isTakeoffStand", + "state": "takeoffStand" + }, + { + "var": "isTakeoffRun", + "state": "TAKEOFFRUN" + }, + { + "var": "isInAirStand", + "state": "inAirStand" + }, + { + "var": "isInAirRun", + "state": "INAIRRUN" + }, + { + "var": "isMovingRightHmd", + "state": "strafeRightHmd" + }, + { + "var": "isMovingLeftHmd", + "state": "strafeLeftHmd" + } + ] + }, + { + "id": "STRAFERIGHT", + "interpTarget": 25, + "interpDuration": 8, + "interpType": "snapshotPrev", + "transitions": [ + { + "var": "isNotMoving", + "state": "idleSettle" + }, + { + "var": "isMovingForward", + "state": "WALKFWD" + }, + { + "var": "isMovingBackward", + "state": "WALKBWD" + }, + { + "var": "isMovingLeft", + "state": "STRAFELEFT" + }, + { + "var": "isTurningRight", + "state": "turnRight" + }, + { + "var": "isTurningLeft", + "state": "turnLeft" + }, + { + "var": "isFlying", + "state": "fly" + }, + { + "var": "isTakeoffStand", + "state": "takeoffStand" + }, + { + "var": "isTakeoffRun", + "state": "TAKEOFFRUN" + }, + { + "var": "isInAirStand", + "state": "inAirStand" + }, + { + "var": "isInAirRun", + "state": "INAIRRUN" + }, + { + "var": "isMovingRightHmd", + "state": "strafeRightHmd" + }, + { + "var": "isMovingLeftHmd", + "state": "strafeLeftHmd" + } + ] + }, + { + "id": "STRAFELEFT", + "interpTarget": 25, + "interpDuration": 8, + "interpType": "snapshotPrev", + "transitions": [ + { + "var": "isNotMoving", + "state": "idleSettle" + }, + { + "var": "isMovingForward", + "state": "WALKFWD" + }, + { + "var": "isMovingBackward", + "state": "WALKBWD" + }, + { + "var": "isMovingRight", + "state": "STRAFERIGHT" + }, + { + "var": "isTurningRight", + "state": "turnRight" + }, + { + "var": "isTurningLeft", + "state": "turnLeft" + }, + { + "var": "isFlying", + "state": "fly" + }, + { + "var": "isTakeoffStand", + "state": "takeoffStand" + }, + { + "var": "isTakeoffRun", + "state": "TAKEOFFRUN" + }, + { + "var": "isInAirStand", + "state": "inAirStand" + }, + { + "var": "isInAirRun", + "state": "INAIRRUN" + }, + { + "var": "isMovingRightHmd", + "state": "strafeRightHmd" + }, + { + "var": "isMovingLeftHmd", + "state": "strafeLeftHmd" + } + ] + }, + { + "id": "turnRight", + "interpTarget": 6, + "interpDuration": 8, + "transitions": [ + { + "var": "isNotTurning", + "state": "idle" + }, + { + "var": "isMovingForward", + "state": "WALKFWD" + }, + { + "var": "isMovingBackward", + "state": "WALKBWD" + }, + { + "var": "isMovingRight", + "state": "STRAFERIGHT" + }, + { + "var": "isMovingLeft", + "state": "STRAFELEFT" + }, + { + "var": "isTurningLeft", + "state": "turnLeft" + }, + { + "var": "isFlying", + "state": "fly" + }, + { + "var": "isTakeoffStand", + "state": "takeoffStand" + }, + { + "var": "isTakeoffRun", + "state": "TAKEOFFRUN" + }, + { + "var": "isInAirStand", + "state": "inAirStand" + }, + { + "var": "isInAirRun", + "state": "INAIRRUN" + }, + { + "var": "isMovingRightHmd", + "state": "strafeRightHmd" + }, + { + "var": "isMovingLeftHmd", + "state": "strafeLeftHmd" + } + ] + }, + { + "id": "turnLeft", + "interpTarget": 6, + "interpDuration": 8, + "transitions": [ + { + "var": "isNotTurning", + "state": "idle" + }, + { + "var": "isMovingForward", + "state": "WALKFWD" + }, + { + "var": "isMovingBackward", + "state": "WALKBWD" + }, + { + "var": "isMovingRight", + "state": "STRAFERIGHT" + }, + { + "var": "isMovingLeft", + "state": "STRAFELEFT" + }, + { + "var": "isTurningRight", + "state": "turnRight" + }, + { + "var": "isFlying", + "state": "fly" + }, + { + "var": "isTakeoffStand", + "state": "takeoffStand" + }, + { + "var": "isTakeoffRun", + "state": "TAKEOFFRUN" + }, + { + "var": "isInAirStand", + "state": "inAirStand" + }, + { + "var": "isInAirRun", + "state": "INAIRRUN" + }, + { + "var": "isMovingRightHmd", + "state": "strafeRightHmd" + }, + { + "var": "isMovingLeftHmd", + "state": "strafeLeftHmd" + } + ] + }, + { + "id": "strafeRightHmd", + "interpTarget": 5, + "interpDuration": 8, + "interpType": "snapshotPrev", + "transitions": [ + { + "var": "isNotMoving", + "state": "idleSettle" + }, + { + "var": "isMovingForward", + "state": "WALKFWD" + }, + { + "var": "isMovingBackward", + "state": "WALKBWD" + }, + { + "var": "isMovingLeftHmd", + "state": "strafeLeftHmd" + }, + { + "var": "isMovingRight", + "state": "STRAFERIGHT" + }, + { + "var": "isMovingLeft", + "state": "STRAFELEFT" + }, + { + "var": "isTurningRight", + "state": "turnRight" + }, + { + "var": "isTurningLeft", + "state": "turnLeft" + }, + { + "var": "isFlying", + "state": "fly" + }, + { + "var": "isTakeoffStand", + "state": "takeoffStand" + }, + { + "var": "isTakeoffRun", + "state": "TAKEOFFRUN" + }, + { + "var": "isInAirStand", + "state": "inAirStand" + }, + { + "var": "isInAirRun", + "state": "INAIRRUN" + } + ] + }, + { + "id": "strafeLeftHmd", + "interpTarget": 5, + "interpDuration": 8, + "interpType": "snapshotPrev", + "transitions": [ + { + "var": "isNotMoving", + "state": "idleSettle" + }, + { + "var": "isMovingForward", + "state": "WALKFWD" + }, + { + "var": "isMovingBackward", + "state": "WALKBWD" + }, + { + "var": "isMovingRightHmd", + "state": "strafeRightHmd" + }, + { + "var": "isMovingRight", + "state": "STRAFERIGHT" + }, + { + "var": "isMovingLeft", + "state": "STRAFELEFT" + }, + { + "var": "isTurningRight", + "state": "turnRight" + }, + { + "var": "isTurningLeft", + "state": "turnLeft" + }, + { + "var": "isFlying", + "state": "fly" + }, + { + "var": "isTakeoffStand", + "state": "takeoffStand" + }, + { + "var": "isTakeoffRun", + "state": "TAKEOFFRUN" + }, + { + "var": "isInAirStand", + "state": "inAirStand" + }, + { + "var": "isInAirRun", + "state": "INAIRRUN" + } + ] + }, + { + "id": "fly", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { + "var": "isNotFlying", + "state": "idleSettle" + } + ] + }, + { + "id": "takeoffStand", + "interpTarget": 2, + "interpDuration": 2, + "transitions": [ + { + "var": "isNotTakeoff", + "state": "inAirStand" + } + ] + }, + { + "id": "TAKEOFFRUN", + "interpTarget": 2, + "interpDuration": 2, + "transitions": [ + { + "var": "isNotTakeoff", + "state": "INAIRRUN" + } + ] + }, + { + "id": "inAirStand", + "interpTarget": 3, + "interpDuration": 3, + "interpType": "snapshotPrev", + "transitions": [ + { + "var": "isNotInAir", + "state": "landStandImpact" + } + ] + }, + { + "id": "INAIRRUN", + "interpTarget": 3, + "interpDuration": 3, + "interpType": "snapshotPrev", + "transitions": [ + { + "var": "isNotInAir", + "state": "WALKFWD" + } + ] + }, + { + "id": "landStandImpact", + "interpTarget": 1, + "interpDuration": 1, + "transitions": [ + { + "var": "isFlying", + "state": "fly" + }, + { + "var": "isTakeoffStand", + "state": "takeoffStand" + }, + { + "var": "isTakeoffRun", + "state": "TAKEOFFRUN" + }, + { + "var": "landStandImpactOnDone", + "state": "landStand" + } + ] + }, + { + "id": "landStand", + "interpTarget": 1, + "interpDuration": 1, + "transitions": [ + { + "var": "isMovingForward", + "state": "WALKFWD" + }, + { + "var": "isMovingBackward", + "state": "WALKBWD" + }, + { + "var": "isMovingRight", + "state": "STRAFERIGHT" + }, + { + "var": "isMovingLeft", + "state": "STRAFELEFT" + }, + { + "var": "isTurningRight", + "state": "turnRight" + }, + { + "var": "isTurningLeft", + "state": "turnLeft" + }, + { + "var": "isFlying", + "state": "fly" + }, + { + "var": "isTakeoffStand", + "state": "takeoffStand" + }, + { + "var": "isTakeoffRun", + "state": "TAKEOFFRUN" + }, + { + "var": "isInAirStand", + "state": "inAirStand" + }, + { + "var": "isInAirRun", + "state": "INAIRRUN" + }, + { + "var": "landStandOnDone", + "state": "idle" + }, + { + "var": "isMovingRightHmd", + "state": "strafeRightHmd" + }, + { + "var": "isMovingLeftHmd", + "state": "strafeLeftHmd" + } + ] + }, + { + "id": "LANDRUN", + "interpTarget": 2, + "interpDuration": 2, + "transitions": [ + { + "var": "isFlying", + "state": "fly" + }, + { + "var": "isTakeoffStand", + "state": "takeoffStand" + }, + { + "var": "isTakeoffRun", + "state": "TAKEOFFRUN" + }, + { + "var": "landRunOnDone", + "state": "WALKFWD" + } + ] + } + ] + }, + "children": [ + { + "id": "idle", + "type": "stateMachine", + "data": { + "currentState": "idleStand", + "states": [ + { + "id": "idleStand", + "interpTarget": 6, + "interpDuration": 10, + "transitions": [ + { + "var": "isTalking", + "state": "idleTalk" + } + ] + }, + { + "id": "idleTalk", + "interpTarget": 6, + "interpDuration": 10, + "transitions": [ + { + "var": "notIsTalking", + "state": "idleStand" + } + ] + } + ] + }, + "children": [ + { + "id": "idleStand", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/idle.fbx", + "startFrame": 0.0, + "endFrame": 300.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "idleTalk", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/talk.fbx", + "startFrame": 0.0, + "endFrame": 800.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + }, + { + "id": "WALKFWD", + "type": "blendLinearMove", + "data": { + "alpha": 0.0, + "desiredSpeed": 1.4, + "characteristicSpeeds": [ 0.5, 1.8, 2.3, 3.2, 4.5 ], + "alphaVar": "moveForwardAlpha", + "desiredSpeedVar": "moveForwardSpeed" + }, + "children": [ + { + "id": "walkFwdShort_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/walk_short_fwd.fbx", + "startFrame": 0.0, + "endFrame": 39.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "walkFwdNormal_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/walk_fwd.fbx", + "startFrame": 0.0, + "endFrame": 30.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "walkFwdFast_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/walk_fwd_fast.fbx", + "startFrame": 0.0, + "endFrame": 25.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "walkFwdJog_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/jog_fwd.fbx", + "startFrame": 0.0, + "endFrame": 25.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "walkFwdRun_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/run_fwd.fbx", + "startFrame": 0.0, + "endFrame": 21.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + }, + { + "id": "idleToWalkFwd", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/idle_to_walk.fbx", + "startFrame": 1.0, + "endFrame": 13.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "idleSettle", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/settle_to_idle.fbx", + "startFrame": 1.0, + "endFrame": 59.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "WALKBWD", + "type": "blendLinearMove", + "data": { + "alpha": 0.0, + "desiredSpeed": 1.4, + "characteristicSpeeds": [ 0.6, 1.6, 2.3, 3.1 ], + "alphaVar": "moveBackwardAlpha", + "desiredSpeedVar": "moveBackwardSpeed" + }, + "children": [ + { + "id": "walkBwdShort_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/walk_short_bwd.fbx", + "startFrame": 0.0, + "endFrame": 38.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "walkBwdFast_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/walk_bwd_fast.fbx", + "startFrame": 0.0, + "endFrame": 27.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "jogBwd_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/jog_bwd.fbx", + "startFrame": 0.0, + "endFrame": 24.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "runBwd_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/run_bwd.fbx", + "startFrame": 0.0, + "endFrame": 16.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + }, + { + "id": "turnLeft", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/turn_left.fbx", + "startFrame": 0.0, + "endFrame": 32.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "turnRight", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/turn_left.fbx", + "startFrame": 0.0, + "endFrame": 32.0, + "timeScale": 1.0, + "loopFlag": true, + "mirrorFlag": true + }, + "children": [] + }, + { + "id": "STRAFELEFT", + "type": "blendLinearMove", + "data": { + "alpha": 0.0, + "desiredSpeed": 1.4, + "characteristicSpeeds": [ 0.1, 0.5, 1.0, 2.6, 3.0 ], + "alphaVar": "moveLateralAlpha", + "desiredSpeedVar": "moveLateralSpeed" + }, + "children": [ + { + "id": "strafeLeftShortStep_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/side_step_short_left.fbx", + "startFrame": 0.0, + "endFrame": 29.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "strafeLeftStep_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/side_step_left.fbx", + "startFrame": 0.0, + "endFrame": 20.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "strafeLeftWalk_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/walk_left.fbx", + "startFrame": 0.0, + "endFrame": 35.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "strafeLeftWalkFast_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/walk_left_fast.fbx", + "startFrame": 0.0, + "endFrame": 21.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "strafeLeftJog_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/jog_left.fbx", + "startFrame": 0.0, + "endFrame": 24.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + }, + { + "id": "STRAFERIGHT", + "type": "blendLinearMove", + "data": { + "alpha": 0.0, + "desiredSpeed": 1.4, + "characteristicSpeeds": [ 0.1, 0.5, 1.0, 2.6, 3.0 ], + "alphaVar": "moveLateralAlpha", + "desiredSpeedVar": "moveLateralSpeed" + }, + "children": [ + { + "id": "strafeRightShortStep_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/side_step_short_left.fbx", + "startFrame": 0.0, + "endFrame": 29.0, + "timeScale": 1.0, + "loopFlag": true, + "mirrorFlag": true + }, + "children": [] + }, + { + "id": "strafeRightStep_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/side_step_left.fbx", + "startFrame": 0.0, + "endFrame": 20.0, + "timeScale": 1.0, + "loopFlag": true, + "mirrorFlag": true + }, + "children": [] + }, + { + "id": "strafeRightWalk_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/walk_left.fbx", + "startFrame": 0.0, + "endFrame": 35.0, + "timeScale": 1.0, + "loopFlag": true, + "mirrorFlag": true + }, + "children": [] + }, + { + "id": "strafeRightFast_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/walk_left_fast.fbx", + "startFrame": 0.0, + "endFrame": 21.0, + "timeScale": 1.0, + "loopFlag": true, + "mirrorFlag": true + }, + "children": [] + }, + { + "id": "strafeRightJog_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/jog_left.fbx", + "startFrame": 0.0, + "endFrame": 24.0, + "timeScale": 1.0, + "loopFlag": true, + "mirrorFlag": true + }, + "children": [] + } + ] + }, + { + "id": "strafeLeftHmd", + "type": "blendLinearMove", + "data": { + "alpha": 0.0, + "desiredSpeed": 1.4, + "characteristicSpeeds": [ 0, 0.5, 2.5 ], + "alphaVar": "moveLateralAlpha", + "desiredSpeedVar": "moveLateralSpeed" + }, + "children": [ + { + "id": "stepLeftShort_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/side_step_short_left.fbx", + "startFrame": 0.0, + "endFrame": 29.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "stepLeft_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/side_step_left.fbx", + "startFrame": 0.0, + "endFrame": 20.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "strafeLeftAnim_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/side_step_left_fast.fbx", + "startFrame": 0.0, + "endFrame": 16.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + }, + { + "id": "strafeRightHmd", + "type": "blendLinearMove", + "data": { + "alpha": 0.0, + "desiredSpeed": 1.4, + "characteristicSpeeds": [ 0, 0.5, 2.5 ], + "alphaVar": "moveLateralAlpha", + "desiredSpeedVar": "moveLateralSpeed" + }, + "children": [ + { + "id": "stepRightShort_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/side_step_short_left.fbx", + "startFrame": 0.0, + "endFrame": 29.0, + "timeScale": 1.0, + "loopFlag": true, + "mirrorFlag": true + }, + "children": [] + }, + { + "id": "stepRight_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/side_step_left.fbx", + "startFrame": 0.0, + "endFrame": 20.0, + "timeScale": 1.0, + "loopFlag": true, + "mirrorFlag": true + }, + "children": [] + }, + { + "id": "strafeRightAnim_c", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/side_step_left_fast.fbx", + "startFrame": 0.0, + "endFrame": 16.0, + "timeScale": 1.0, + "loopFlag": true, + "mirrorFlag": true + }, + "children": [] + } + ] + }, + { + "id": "fly", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/fly.fbx", + "startFrame": 1.0, + "endFrame": 80.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "takeoffStand", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/jump_standing_launch.fbx", + "startFrame": 2.0, + "endFrame": 16.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "TAKEOFFRUN", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/jump_running_launch_land.fbx", + "startFrame": 4.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "inAirStand", + "type": "blendLinear", + "data": { + "alpha": 0.0, + "alphaVar": "inAirAlpha" + }, + "children": [ + { + "id": "inAirStandPreApex", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/jump_standing_apex.fbx", + "startFrame": 0.0, + "endFrame": 0.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "inAirStandApex", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/jump_standing_apex.fbx", + "startFrame": 1.0, + "endFrame": 1.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "inAirStandPostApex", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/jump_standing_apex.fbx", + "startFrame": 2.0, + "endFrame": 2.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + } + ] + }, + { + "id": "INAIRRUN", + "type": "blendLinear", + "data": { + "alpha": 0.0, + "alphaVar": "inAirAlpha" + }, + "children": [ + { + "id": "inAirRunPreApex", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/jump_running_launch_land.fbx", + "startFrame": 16.0, + "endFrame": 16.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "inAirRunApex", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/jump_running_launch_land.fbx", + "startFrame": 22.0, + "endFrame": 22.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "inAirRunPostApex", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/jump_running_launch_land.fbx", + "startFrame": 33.0, + "endFrame": 33.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + } + ] + }, + { + "id": "landStandImpact", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/jump_standing_land_settle.fbx", + "startFrame": 1.0, + "endFrame": 6.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "landStand", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/jump_standing_land_settle.fbx", + "startFrame": 6.0, + "endFrame": 68.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "LANDRUN", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/jump_running_launch_land.fbx", + "startFrame": 29.0, + "endFrame": 40.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "id": "userAnimA", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/idle.fbx", + "startFrame": 0.0, + "endFrame": 90.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "userAnimB", + "type": "clip", + "data": { + "url": "qrc:///avatar/animations/idle.fbx", + "startFrame": 0.0, + "endFrame": 90.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + } +} diff --git a/interface/resources/controllers/touchscreenvirtualpad.json b/interface/resources/controllers/touchscreenvirtualpad.json index bae1172152..e98fb7ff2f 100644 --- a/interface/resources/controllers/touchscreenvirtualpad.json +++ b/interface/resources/controllers/touchscreenvirtualpad.json @@ -23,7 +23,9 @@ "invert" ], "to": "Actions.Pitch" - } + }, + + { "from": "TouchscreenVirtualPad.RB", "to": "Standard.RB"} ] } diff --git a/interface/resources/fonts/rawline-500.ttf b/interface/resources/fonts/rawline-500.ttf new file mode 100644 index 0000000000..a0e18c7364 Binary files /dev/null and b/interface/resources/fonts/rawline-500.ttf differ diff --git a/interface/resources/images/handshake.png b/interface/resources/images/handshake.png new file mode 100644 index 0000000000..ae4252e9e5 Binary files /dev/null and b/interface/resources/images/handshake.png differ diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index a1b89e1529..9a9252112c 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -425,11 +425,12 @@ FocusScope { console.warn("Could not find top level window for " + item); return; } - +/* if (typeof Controller === "undefined") { console.warn("Controller not yet available... can't center"); return; } +*/ var newRecommendedRectJS = (typeof Controller === "undefined") ? Qt.rect(0,0,0,0) : Controller.getRecommendedHUDRect(); var newRecommendedRect = Qt.rect(newRecommendedRectJS.x, newRecommendedRectJS.y, @@ -455,15 +456,17 @@ FocusScope { console.warn("Could not find top level window for " + item); return; } - +/* if (typeof Controller === "undefined") { console.warn("Controller not yet available... can't reposition targetWindow:" + targetWindow); return; } +*/ var oldRecommendedRect = recommendedRect; var oldRecommendedDimmensions = { x: oldRecommendedRect.width, y: oldRecommendedRect.height }; - var newRecommendedRect = Controller.getRecommendedHUDRect(); + var newRecommendedRect = { width: 1280, height: 720, x: 0, y: 0 }; + if (typeof Controller !== "undefined") newRecommendedRect = Controller.getRecommendedHUDRect(); var newRecommendedDimmensions = { x: newRecommendedRect.width, y: newRecommendedRect.height }; repositionWindow(targetWindow, false, oldRecommendedRect, oldRecommendedDimmensions, newRecommendedRect, newRecommendedDimmensions); } @@ -480,7 +483,8 @@ FocusScope { return; } - var recommended = Controller.getRecommendedHUDRect(); + var recommended = { width: 1280, height: 720, x: 0, y: 0 }; + if (typeof Controller !== "undefined") recommended = Controller.getRecommendedHUDRect(); var maxX = recommended.x + recommended.width; var maxY = recommended.y + recommended.height; var newPosition = Qt.vector2d(targetWindow.x, targetWindow.y); diff --git a/interface/resources/qml/dialogs/CustomQueryDialog.qml b/interface/resources/qml/dialogs/CustomQueryDialog.qml index 026068eee1..2497781db0 100644 --- a/interface/resources/qml/dialogs/CustomQueryDialog.qml +++ b/interface/resources/qml/dialogs/CustomQueryDialog.qml @@ -273,11 +273,7 @@ ModalWindow { onTriggered: { root.result = null; root.canceled(); - // FIXME we are leaking memory to avoid a crash - // root.destroy(); - - root.disableFade = true - visible = false; + root.destroy(); } } @@ -299,11 +295,7 @@ ModalWindow { } root.result = JSON.stringify(result); root.selected(root.result); - // FIXME we are leaking memory to avoid a crash - // root.destroy(); - - root.disableFade = true - visible = false; + root.destroy(); } } } diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index c1509e0fc1..ba5e162391 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -815,7 +815,7 @@ ModalWindow { Action { id: cancelAction text: "Cancel" - onTriggered: { canceled(); root.shown = false; } + onTriggered: { canceled(); root.destroy(); } } } diff --git a/interface/resources/qml/dialogs/QueryDialog.qml b/interface/resources/qml/dialogs/QueryDialog.qml index 9cfb3011bd..41ded7e934 100644 --- a/interface/resources/qml/dialogs/QueryDialog.qml +++ b/interface/resources/qml/dialogs/QueryDialog.qml @@ -168,11 +168,7 @@ ModalWindow { shortcut: "Esc" onTriggered: { root.canceled(); - // FIXME we are leaking memory to avoid a crash - // root.destroy(); - - root.disableFade = true - visible = false; + root.destroy(); } } @@ -183,11 +179,7 @@ ModalWindow { onTriggered: { root.result = items ? comboBox.currentText : textResult.text root.selected(root.result); - // FIXME we are leaking memory to avoid a crash - // root.destroy(); - - root.disableFade = true - visible = false; + root.destroy(); } } } diff --git a/interface/resources/qml/hifi/Desktop.qml b/interface/resources/qml/hifi/Desktop.qml index c44ebdbab1..a97d94d91c 100644 --- a/interface/resources/qml/hifi/Desktop.qml +++ b/interface/resources/qml/hifi/Desktop.qml @@ -18,8 +18,8 @@ OriginalDesktop.Desktop { hoverEnabled: true propagateComposedEvents: true scrollGestureEnabled: false // we don't need/want these - onEntered: ApplicationCompositor.reticleOverDesktop = true - onExited: ApplicationCompositor.reticleOverDesktop = false + onEntered: if (typeof ApplicationCompositor !== "undefined") ApplicationCompositor.reticleOverDesktop = true + onExited: if (typeof ApplicationCompositor !== "undefined") ApplicationCompositor.reticleOverDesktop = false acceptedButtons: Qt.NoButton } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index 85ef821a4a..bf8c06d1b3 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -213,6 +213,63 @@ Item { popup.open(); } + HiFiGlyphs { + id: errorsGlyph + visible: !AvatarPackagerCore.currentAvatarProject || AvatarPackagerCore.currentAvatarProject.hasErrors + text: hifi.glyphs.alert + size: 315 + color: "#EA4C5F" + anchors { + top: parent.top + topMargin: -30 + horizontalCenter: parent.horizontalCenter + } + } + + Image { + id: successGlyph + visible: AvatarPackagerCore.currentAvatarProject && !AvatarPackagerCore.currentAvatarProject.hasErrors + anchors { + top: parent.top + topMargin: 52 + horizontalCenter: parent.horizontalCenter + } + width: 149.6 + height: 149 + source: "../../../icons/checkmark-stroke.svg" + } + + RalewayRegular { + id: doctorStatusMessage + + states: [ + State { + when: AvatarPackagerCore.currentAvatarProject && !AvatarPackagerCore.currentAvatarProject.hasErrors + name: "noErrors" + PropertyChanges { + target: doctorStatusMessage + text: "Your avatar looks fine." + } + }, + State { + when: !AvatarPackagerCore.currentAvatarProject || AvatarPackagerCore.currentAvatarProject.hasErrors + name: "errors" + PropertyChanges { + target: doctorStatusMessage + text: "Your avatar has a few issues." + } + } + ] + color: 'white' + size: 20 + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: errorsGlyph.bottom + + wrapMode: Text.Wrap + } + RalewayRegular { id: infoMessage @@ -240,7 +297,7 @@ Item { anchors.left: parent.left anchors.right: parent.right - anchors.top: parent.top + anchors.top: doctorStatusMessage.bottom anchors.bottomMargin: 24 @@ -249,6 +306,53 @@ Item { text: "You can upload your files to our servers to always access them, and to make your avatar visible to other users." } + RalewayRegular { + id: notForSaleMessage + + visible: root.hasSuccessfullyUploaded + + color: 'white' + linkColor: '#00B4EF' + size: 20 + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: infoMessage.bottom + anchors.topMargin: 10 + + anchors.bottomMargin: 24 + + wrapMode: Text.Wrap + text: "This item is not for sale yet, learn more." + + onLinkActivated: { + Qt.openUrlExternally("https://docs.highfidelity.com/sell/add-item/upload-avatar.html"); + } + } + + RalewayRegular { + id: showErrorsLink + + color: 'white' + linkColor: '#00B4EF' + + visible: AvatarPackagerCore.currentAvatarProject && AvatarPackagerCore.currentAvatarProject.hasErrors + + anchors { + top: notForSaleMessage.bottom + topMargin: 16 + horizontalCenter: parent.horizontalCenter + } + + size: 28 + + text: "View all errors" + + onLinkActivated: { + avatarPackager.state = AvatarPackagerState.avatarDoctorErrorReport; + } + } + HifiControls.Button { id: openFolderButton diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index 2d5f77f006..3ace6f381f 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -33,15 +33,19 @@ Rectangle { property bool balanceReceived: false; property bool availableUpdatesReceived: false; property bool itemInfoReceived: false; + property bool dataReady: itemInfoReceived && ownershipStatusReceived && balanceReceived && availableUpdatesReceived; property string baseItemName: ""; property string itemName; property string itemId; property string itemHref; property string itemAuthor; property int itemEdition: -1; + property bool hasSomethingToTradeIn: itemEdition > 0; // i.e., don't trade in your artist's proof + property bool isTradingIn: canUpdate && hasSomethingToTradeIn; + property bool isStocking: (availability === 'not for sale') && (creator === Account.username) && !updated_item_id; property string certificateId; property double balanceAfterPurchase; - property bool alreadyOwned: false; + property bool alreadyOwned: false; // Including proofs property int itemPrice: -1; property bool isCertified; property string itemType: "unknown"; @@ -55,7 +59,10 @@ Rectangle { property bool canRezCertifiedItems: Entities.canRezCertified() || Entities.canRezTmpCertified(); property string referrer; property bool isInstalled; - property bool isUpdating; + property bool canUpdate; + property string availability: "available"; + property string updated_item_id: ""; + property string creator: ""; property string baseAppURL; property int currentUpdatesPage: 1; // Style @@ -138,6 +145,7 @@ Rectangle { } onAvailableUpdatesResult: { + // Answers the updatable original item cert data still owned by this user that are EITHER instances of this marketplace id, or update to this marketplace id. if (result.status !== 'success') { console.log("Failed to get Available Updates", result.data.message); } else { @@ -149,7 +157,7 @@ Rectangle { if (root.itemEdition !== -1 && root.itemEdition !== parseInt(result.data.updates[i].edition_number)) { continue; } - root.isUpdating = true; + root.canUpdate = true; root.baseItemName = result.data.updates[i].base_item_title; // This CertID is the one corresponding to the base item CertID that the user already owns root.certificateId = result.data.updates[i].certificate_id; @@ -160,7 +168,7 @@ Rectangle { } } - if (result.data.updates.length === 0 || root.isUpdating) { + if (result.data.updates.length === 0 || root.canUpdate) { root.availableUpdatesReceived = true; refreshBuyUI(); } else { @@ -172,7 +180,7 @@ Rectangle { onUpdateItemResult: { if (result.status !== 'success') { - failureErrorText.text = result.message; + failureErrorText.text = result.data ? (result.data.message || "Unknown Error") : JSON.stringify(result); root.activeView = "checkoutFailure"; } else { root.itemHref = result.data.download_url; @@ -260,13 +268,6 @@ Rectangle { } } } - MouseArea { - enabled: titleBarContainer.usernameDropdownVisible; - anchors.fill: parent; - onClicked: { - titleBarContainer.usernameDropdownVisible = false; - } - } // // TITLE BAR END // @@ -434,7 +435,7 @@ Rectangle { anchors.top: parent.top; anchors.left: itemPreviewImage.right; anchors.leftMargin: 12; - anchors.right: itemPriceContainer.left; + anchors.right: parent.right; anchors.rightMargin: 8; height: 30; // Style @@ -449,21 +450,22 @@ Rectangle { Item { id: itemPriceContainer; // Anchors - anchors.top: parent.top; - anchors.right: parent.right; + anchors.top: itemNameText.bottom; + anchors.topMargin: 8; + anchors.left: itemNameText.left; height: 30; - width: itemPriceTextLabel.width + itemPriceText.width + 20; + width: itemPriceText.width + 20; - // "HFC" balance label + // "HFC" label HiFiGlyphs { id: itemPriceTextLabel; - visible: !(root.isUpdating && root.itemEdition > 0) && (root.itemPrice > 0); + visible: !isTradingIn && (root.itemPrice > 0); text: hifi.glyphs.hfc; // Size size: 30; // Anchors - anchors.right: itemPriceText.left; - anchors.rightMargin: 4; + anchors.right: parent.right; + //anchors.rightMargin: 4; anchors.top: parent.top; anchors.topMargin: 0; width: paintedWidth; @@ -473,13 +475,15 @@ Rectangle { } FiraSansSemiBold { id: itemPriceText; - text: (root.isUpdating && root.itemEdition > 0) ? "FREE\nUPDATE" : ((root.itemPrice === -1) ? "--" : ((root.itemPrice > 0) ? root.itemPrice : "FREE")); + text: isTradingIn ? "FREE\nUPDATE" : + (isStocking ? "Free for creator" : + ((root.itemPrice === -1) ? "--" : ((root.itemPrice > 0) ? root.itemPrice : "FREE"))); // Text size - size: (root.isUpdating && root.itemEdition > 0) ? 20 : 26; + size: isTradingIn ? 20 : 26; // Anchors anchors.top: parent.top; - anchors.right: parent.right; - anchors.rightMargin: 16; + anchors.left: itemPriceTextLabel.visible ? itemPriceTextLabel.right : parent.left; + anchors.leftMargin: 4; width: paintedWidth; height: paintedHeight; // Style @@ -571,7 +575,7 @@ Rectangle { // "View in Inventory" button HifiControlsUit.Button { id: viewInMyPurchasesButton; - visible: false; + visible: isCertified && dataReady && (isTradingIn ? hasSomethingToTradeIn : alreadyOwned); color: hifi.buttons.blue; colorScheme: hifi.colorSchemes.light; anchors.top: buyTextContainer.visible ? buyTextContainer.bottom : checkoutActionButtonsContainer.top; @@ -579,9 +583,9 @@ Rectangle { height: 50; anchors.left: parent.left; anchors.right: parent.right; - text: root.isUpdating ? "UPDATE TO THIS ITEM FOR FREE" : "VIEW THIS ITEM IN YOUR INVENTORY"; + text: (canUpdate && !isTradingIn) ? "UPDATE TO THIS ITEM FOR FREE" : "VIEW THIS ITEM IN YOUR INVENTORY"; onClicked: { - if (root.isUpdating) { + if (root.canUpdate) { sendToScript({method: 'checkout_goToPurchases', filterText: root.baseItemName}); } else { sendToScript({method: 'checkout_goToPurchases', filterText: root.itemName}); @@ -592,8 +596,12 @@ Rectangle { // "Buy" button HifiControlsUit.Button { id: buyButton; - visible: !((root.itemType === "avatar" || root.itemType === "app") && viewInMyPurchasesButton.visible) - enabled: (root.balanceAfterPurchase >= 0 && ownershipStatusReceived && balanceReceived && availableUpdatesReceived) || (!root.isCertified) || root.isUpdating; + visible: isTradingIn || !alreadyOwned || isStocking || !(root.itemType === "avatar" || root.itemType === "app"); + property bool checkBalance: dataReady && (root.availability === "available") + enabled: (checkBalance && (balanceAfterPurchase >= 0)) || !isCertified || isTradingIn || isStocking; + text: isTradingIn ? "Confirm Update" : + (enabled ? (viewInMyPurchasesButton.visible ? "Get It Again" : (dataReady ? "Get Item" : "--")) : + (checkBalance ? "Insufficient Funds" : availability)) color: viewInMyPurchasesButton.visible ? hifi.buttons.white : hifi.buttons.blue; colorScheme: hifi.colorSchemes.light; anchors.top: viewInMyPurchasesButton.visible ? viewInMyPurchasesButton.bottom : @@ -602,10 +610,8 @@ Rectangle { height: 50; anchors.left: parent.left; anchors.right: parent.right; - text: (root.isUpdating && root.itemEdition > 0) ? "CONFIRM UPDATE" : (((root.isCertified) ? ((ownershipStatusReceived && balanceReceived && availableUpdatesReceived) ? - ((viewInMyPurchasesButton.visible && !root.isUpdating) ? "Get It Again" : "Confirm") : "--") : "Get Item")); onClicked: { - if (root.isUpdating && root.itemEdition > 0) { + if (isTradingIn) { // If we're updating an app, the existing app needs to be uninstalled. // This call will fail/return `false` if the app isn't installed, but that's OK. if (root.itemType === "app") { @@ -1063,7 +1069,11 @@ Rectangle { buyButton.color = hifi.buttons.red; root.shouldBuyWithControlledFailure = true; } else { - buyButton.text = (root.isCertified ? ((ownershipStatusReceived && balanceReceived && availableUpdatesReceived) ? (root.alreadyOwned ? "Buy Another" : "Buy"): "--") : "Get Item"); + buyButton.text = (root.isCertified ? + (dataReady ? + (root.alreadyOwned ? "Buy Another" : "Buy") : + "--") : + "Get Item"); buyButton.color = hifi.buttons.blue; root.shouldBuyWithControlledFailure = false; } @@ -1091,6 +1101,9 @@ Rectangle { root.itemPrice = result.data.cost; root.itemAuthor = result.data.creator; root.itemType = result.data.item_type || "unknown"; + root.availability = result.data.availability; + root.updated_item_id = result.data.updated_item_id || "" + root.creator = result.data.creator; if (root.itemType === "unknown") { root.itemHref = result.data.review_url; } else { @@ -1139,7 +1152,7 @@ Rectangle { signal sendToScript(var message); function canBuyAgain() { - return (root.itemType === "entity" || root.itemType === "wearable" || root.itemType === "contentSet" || root.itemType === "unknown"); + return root.itemType === "entity" || root.itemType === "wearable" || root.itemType === "contentSet" || root.itemType === "unknown" || isStocking; } function handleContentSets() { @@ -1185,29 +1198,23 @@ Rectangle { function refreshBuyUI() { if (root.isCertified) { - if (root.ownershipStatusReceived && root.balanceReceived && root.availableUpdatesReceived) { + if (dataReady) { buyText.text = ""; // If the user IS on the checkout page for the updated version of an owned item... - if (root.isUpdating) { + if (root.canUpdate) { // If the user HAS already selected a specific edition to update... - if (root.itemEdition > 0) { + if (hasSomethingToTradeIn) { buyText.text = "By pressing \"Confirm Update\", you agree to trade in your old item for the updated item that replaces it."; buyTextContainer.color = "#FFFFFF"; buyTextContainer.border.color = "#FFFFFF"; // Else if the user HAS NOT selected a specific edition to update... } else { - viewInMyPurchasesButton.visible = true; - handleBuyAgainLogic(); } // If the user IS NOT on the checkout page for the updated verison of an owned item... // (i.e. they are checking out an item "normally") } else { - if (root.alreadyOwned) { - viewInMyPurchasesButton.visible = true; - } - handleBuyAgainLogic(); } } else { diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml index cdb8368296..4ff935921f 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -41,6 +41,7 @@ Rectangle { property string searchScopeString: "Featured" property bool isLoggedIn: false property bool supports3DHTML: true + property bool pendingGetMarketplaceItemCall: false anchors.fill: (typeof parent === undefined) ? undefined : parent @@ -100,7 +101,9 @@ Rectangle { getMarketplaceItems(); } onGetMarketplaceItemsResult: { - marketBrowseModel.handlePage(result.status !== "success" && result.message, result); + if (!pendingGetMarketplaceItemCall) { + marketBrowseModel.handlePage(result.status !== "success" && result.message, result); + } } onGetMarketplaceItemResult: { @@ -112,7 +115,7 @@ Rectangle { marketplaceItem.image_url = result.data.thumbnail_url; marketplaceItem.name = result.data.title; marketplaceItem.likes = result.data.likes; - if(result.data.has_liked !== undefined) { + if (result.data.has_liked !== undefined) { marketplaceItem.liked = result.data.has_liked; } marketplaceItem.creator = result.data.creator; @@ -121,11 +124,13 @@ Rectangle { marketplaceItem.description = result.data.description; marketplaceItem.attributions = result.data.attributions; marketplaceItem.license = result.data.license; - marketplaceItem.available = result.data.availability === "available"; + marketplaceItem.availability = result.data.availability; + marketplaceItem.updated_item_id = result.data.updated_item_id || ""; marketplaceItem.created_at = result.data.created_at; marketplaceItemScrollView.contentHeight = marketplaceItemContent.height; itemsList.visible = false; marketplaceItemView.visible = true; + pendingGetMarketplaceItemCall = false; } } } @@ -539,7 +544,7 @@ Rectangle { creator: model.creator category: model.primary_category price: model.cost - available: model.availability === "available" + availability: model.availability isLoggedIn: root.isLoggedIn; onShowItem: { @@ -711,7 +716,7 @@ Rectangle { topMargin: 10; leftMargin: 15; } - height: visible ? childrenRect.height : 0 + height: visible ? 36 : 0 RalewayRegular { id: sortText @@ -733,8 +738,9 @@ Rectangle { top: parent.top leftMargin: 20 } + width: root.isLoggedIn ? 342 : 262 - height: 36 + height: parent.height radius: 4 border.width: 1 @@ -978,7 +984,6 @@ Rectangle { xhr.open("GET", url); xhr.onreadystatechange = function() { if (xhr.readyState == XMLHttpRequest.DONE) { - console.log(xhr.responseText); licenseText.text = xhr.responseText; licenseInfo.visible = true; } @@ -1223,6 +1228,7 @@ Rectangle { console.log("A message with method 'updateMarketplaceQMLItem' was sent without an itemId!"); return; } + pendingGetMarketplaceItemCall = true; marketplaceItem.edition = message.params.edition ? message.params.edition : -1; MarketplaceScriptingInterface.getMarketplaceItem(message.params.itemId); break; diff --git a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml index 2c7a50033c..fa7e311026 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceItem.qml @@ -2,7 +2,7 @@ // MarketplaceListItem.qml // qml/hifi/commerce/marketplace // -// MarketplaceListItem +// MarketplaceItem // // Created by Roxanne Skelly on 2019-01-22 // Copyright 2019 High Fidelity, Inc. @@ -33,16 +33,16 @@ Rectangle { property string creator: "" property var categories: [] property int price: 0 + property string availability: "unknown" + property string updated_item_id: "" property var attributions: [] property string description: "" property string license: "" property string posted: "" - property bool available: false property string created_at: "" property bool isLoggedIn: false; property int edition: -1; property bool supports3DHTML: false; - onCategoriesChanged: { categoriesListModel.clear(); @@ -52,13 +52,7 @@ Rectangle { } onDescriptionChanged: { - - if(root.supports3DHTML) { - descriptionTextModel.clear(); - descriptionTextModel.append({text: description}); - } else { - descriptionText.text = description; - } + descriptionText.text = description; } onAttributionsChanged: { @@ -250,7 +244,6 @@ Rectangle { function evalHeight() { height = categoriesList.y - buyButton.y + categoriesList.height; - console.log("HEIGHT: " + height); } HifiControlsUit.Button { @@ -264,11 +257,17 @@ Rectangle { } height: 50 - text: root.edition >= 0 ? "UPGRADE FOR FREE" : (root.available ? (root.price ? root.price : "FREE") : "UNAVAILABLE (not for sale)") - enabled: root.edition >= 0 || root.available - buttonGlyph: root.available ? (root.price ? hifi.glyphs.hfc : "") : "" + property bool isUpdate: root.edition >= 0 // Special case of updating from a specific older item + property bool isStocking: (creator === Account.username) && (availability === "not for sale") && !updated_item_id // Note: server will say "sold out" or "invalidated" before it says NFS + property bool isFreeSpecial: isStocking || isUpdate + enabled: isFreeSpecial || (availability === 'available') + buttonGlyph: (enabled && !isUpdate && (price > 0)) ? hifi.glyphs.hfc : "" + text: isUpdate ? "UPDATE FOR FREE" : (isStocking ? "FREE STOCK" : (enabled ? (price || "FREE") : availability)) color: hifi.buttons.blue - + + buttonGlyphSize: 24 + fontSize: 24 + onClicked: root.buy(); } diff --git a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceListItem.qml b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceListItem.qml index 2f37637e40..439247e410 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/MarketplaceListItem.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/MarketplaceListItem.qml @@ -34,7 +34,7 @@ Rectangle { property string creator: "" property string category: "" property int price: 0 - property bool available: false + property string availability: "unknown" property bool isLoggedIn: false; signal buy() @@ -226,6 +226,7 @@ Rectangle { top: parent.top left: parent.left leftMargin: 15 + topMargin: 10 } width: paintedWidth @@ -241,7 +242,7 @@ Rectangle { anchors { top: creatorLabel.top; left: creatorLabel.right; - leftMargin: 15; + leftMargin: 10; } width: paintedWidth; @@ -273,7 +274,7 @@ Rectangle { anchors { top: categoryLabel.top left: categoryLabel.right - leftMargin: 15 + leftMargin: 10 } width: paintedWidth @@ -298,11 +299,21 @@ Rectangle { topMargin:10 bottomMargin: 10 } + width: 180 + + property bool isNFS: availability === "not for sale" // Note: server will say "sold out" or "invalidated" before it says NFS + property bool isMine: creator === Account.username + property bool isUpgrade: root.edition >= 0 + property int costToMe: ((isMine && isNFS) || isUpgrade) ? 0 : price + property bool isAvailable: availability === "available" + + text: isUpgrade ? "UPGRADE FOR FREE" : (isAvailable ? (costToMe || "FREE") : availability) + enabled: isAvailable + buttonGlyph: isAvailable ? (costToMe ? hifi.glyphs.hfc : "") : "" - text: root.price ? root.price : "FREE" - buttonGlyph: root.price ? hifi.glyphs.hfc : "" color: hifi.buttons.blue; - + buttonGlyphSize: 24 + fontSize: 24 onClicked: root.buy(); } } diff --git a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml index df6e216b32..2c2fed1d8f 100644 --- a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml +++ b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml @@ -380,7 +380,7 @@ Item { if (updateButton.visible && uninstallButton.visible) { item.itemButtonText = ""; item.glyphSize = 20; - } else { + } else if (item) { item.itemButtonText = "Send to Trash"; item.glyphSize = 30; } diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index f8e2c9115b..ea74549084 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -40,10 +40,6 @@ Rectangle { source: "images/wallet-bg.jpg"; } - Component.onDestruction: { - KeyboardScriptingInterface.raised = false; - } - Connections { target: Commerce; diff --git a/interface/resources/qml/hifi/tablet/TabletHome.qml b/interface/resources/qml/hifi/tablet/TabletHome.qml index 4d7883522e..a1da69a44a 100644 --- a/interface/resources/qml/hifi/tablet/TabletHome.qml +++ b/interface/resources/qml/hifi/tablet/TabletHome.qml @@ -6,7 +6,7 @@ import QtQuick.Layouts 1.3 import TabletScriptingInterface 1.0 import "." -import stylesUit 1.0 +import stylesUit 1.0 as HifiStylesUit import "../audio" as HifiAudio Item { @@ -49,44 +49,116 @@ Item { } Item { - width: 150 - height: 50 + id: rightContainer + width: clockItem.width > loginItem.width ? clockItem.width + clockAmPmTextMetrics.width : + loginItem.width + clockAmPmTextMetrics.width + height: parent.height + anchors.top: parent.top + anchors.topMargin: 15 anchors.right: parent.right - anchors.rightMargin: 30 - anchors.verticalCenter: parent.verticalCenter + anchors.rightMargin: 20 + anchors.bottom: parent.bottom - ColumnLayout { - anchors.fill: parent + function timeChanged() { + var date = new Date(); + clockTime.text = date.toLocaleTimeString(Qt.locale("en_US"), "h:mm ap"); + var regex = /[\sa-zA-z]+/; + clockTime.text = clockTime.text.replace(regex, ""); + clockAmPm.text = date.toLocaleTimeString(Qt.locale("en_US"), "ap"); + } - RalewaySemiBold { - text: Account.loggedIn ? qsTr("Log out") : qsTr("Log in") - horizontalAlignment: Text.AlignRight - Layout.alignment: Qt.AlignRight - font.pixelSize: 20 - color: "#afafaf" + Timer { + interval: 1000; running: true; repeat: true; + onTriggered: rightContainer.timeChanged(); + } + + Item { + id: clockAmPmItem + width: clockAmPmTextMetrics.width + height: clockAmPmTextMetrics.height + + anchors.top: parent.top + anchors.right: parent.right + TextMetrics { + id: clockAmPmTextMetrics + text: clockAmPm.text + font: clockAmPm.font } - - RalewaySemiBold { - visible: Account.loggedIn - height: Account.loggedIn ? parent.height/2 - parent.spacing/2 : 0 - text: Account.loggedIn ? "[" + tabletRoot.usernameShort + "]" : "" - horizontalAlignment: Text.AlignRight - Layout.alignment: Qt.AlignRight - font.pixelSize: 20 + Text { + anchors.left: parent.left + id: clockAmPm + anchors.right: parent.right + font.capitalization: Font.AllUppercase + font.pixelSize: 12 + font.family: "Rawline" color: "#afafaf" } } - MouseArea { - anchors.fill: parent - onClicked: { - if (!Account.loggedIn) { - DialogsManager.showLoginDialog() - } else { - Account.logOut() + Item { + id: clockItem + width: clockTimeTextMetrics.width + height: clockTimeTextMetrics.height + anchors { + top: parent.top + topMargin: -10 + right: clockAmPmItem.left + rightMargin: 5 + } + TextMetrics { + id: clockTimeTextMetrics + text: clockTime.text + font: clockTime.font + } + Text { + anchors.top: parent.top + anchors.right: parent.right + id: clockTime + font.bold: false + font.pixelSize: 36 + font.family: "Rawline" + color: "#afafaf" + } + } + + Item { + id: loginItem + width: loginTextMetrics.width + height: loginTextMetrics.height + anchors { + bottom: parent.bottom + bottomMargin: 10 + right: clockAmPmItem.left + rightMargin: 5 + } + Text { + id: loginText + anchors.right: parent.right + text: Account.loggedIn ? tabletRoot.usernameShort : qsTr("Log in") + horizontalAlignment: Text.AlignRight + Layout.alignment: Qt.AlignRight + font.pixelSize: 18 + font.family: "Rawline" + color: "#afafaf" + } + TextMetrics { + id: loginTextMetrics + text: loginText.text + font: loginText.font + } + + MouseArea { + anchors.fill: parent + onClicked: { + if (!Account.loggedIn) { + DialogsManager.showLoginDialog(); + } } } } + Component.onCompleted: { + rightContainer.timeChanged(); + } } } diff --git a/interface/resources/qml/hifi/tablet/TabletRoot.qml b/interface/resources/qml/hifi/tablet/TabletRoot.qml index 93a23f1b9d..8d237d146a 100644 --- a/interface/resources/qml/hifi/tablet/TabletRoot.qml +++ b/interface/resources/qml/hifi/tablet/TabletRoot.qml @@ -178,10 +178,10 @@ Rectangle { function setUsername(newUsername) { username = newUsername; - usernameShort = newUsername.substring(0, 8); + usernameShort = newUsername.substring(0, 14); - if (newUsername.length > 8) { - usernameShort = usernameShort + "..." + if (newUsername.length > 14) { + usernameShort = usernameShort + "..." } } diff --git a/interface/resources/qml/styles-uit/Rawline.qml b/interface/resources/qml/styles-uit/Rawline.qml new file mode 100644 index 0000000000..50c6544739 --- /dev/null +++ b/interface/resources/qml/styles-uit/Rawline.qml @@ -0,0 +1,20 @@ +// +// Rawline.qml +// +// Created by Wayne Chen on 25 Feb 2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 + +Text { + id: root + property real size: 32 + font.pixelSize: size + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignLeft + font.family: "Rawline" +} diff --git a/interface/resources/qml/stylesUit/Rawline.qml b/interface/resources/qml/stylesUit/Rawline.qml new file mode 100644 index 0000000000..50c6544739 --- /dev/null +++ b/interface/resources/qml/stylesUit/Rawline.qml @@ -0,0 +1,20 @@ +// +// Rawline.qml +// +// Created by Wayne Chen on 25 Feb 2019 +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.7 + +Text { + id: root + property real size: 32 + font.pixelSize: size + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignLeft + font.family: "Rawline" +} diff --git a/interface/src/AboutUtil.h b/interface/src/AboutUtil.h index c06255aaa5..4a5074857d 100644 --- a/interface/src/AboutUtil.h +++ b/interface/src/AboutUtil.h @@ -16,8 +16,8 @@ #include /**jsdoc - * The HifiAbout API provides information about the version of Interface that is currently running. It also - * provides the ability to open a Web page in an Interface browser window. + * The HifiAbout API provides information about the version of Interface that is currently running. It also + * has the functionality to open a web page in an Interface browser window. * * @namespace HifiAbout * @@ -30,9 +30,9 @@ * @property {string} qtVersion - The Qt version used in Interface that is currently running. Read-only. * * @example Report build information for the version of Interface currently running. - * print("HiFi build date: " + HifiAbout.buildDate); // 11 Feb 2019 - * print("HiFi version: " + HifiAbout.buildVersion); // 0.78.0 - * print("Qt version: " + HifiAbout.qtVersion); // 5.10.1 + * print("HiFi build date: " + HifiAbout.buildDate); // Returns the build date of the version of Interface currently running on your machine. + * print("HiFi version: " + HifiAbout.buildVersion); // Returns the build version of Interface currently running on your machine. + * print("Qt version: " + HifiAbout.qtVersion); // Returns the Qt version details of the version of Interface currently running on your machine. */ class AboutUtil : public QObject { @@ -52,9 +52,9 @@ public: public slots: /**jsdoc - * Display a Web page in an Interface browser window. + * Display a web page in an Interface browser window. * @function HifiAbout.openUrl - * @param {string} url - The URL of the Web page to display. + * @param {string} url - The URL of the web page you want to view in Interface. */ void openUrl(const QString &url) const; private: diff --git a/interface/src/AndroidHelper.cpp b/interface/src/AndroidHelper.cpp index 4f75d5bdb2..e5007d706e 100644 --- a/interface/src/AndroidHelper.cpp +++ b/interface/src/AndroidHelper.cpp @@ -45,6 +45,10 @@ void AndroidHelper::notifyBeforeEnterBackground() { emit beforeEnterBackground(); } +void AndroidHelper::notifyToggleAwayMode() { + emit toggleAwayMode(); +} + void AndroidHelper::notifyEnterBackground() { emit enterBackground(); } diff --git a/interface/src/AndroidHelper.h b/interface/src/AndroidHelper.h index f1cec6a43b..fca035a217 100644 --- a/interface/src/AndroidHelper.h +++ b/interface/src/AndroidHelper.h @@ -31,6 +31,7 @@ public: void notifyEnterForeground(); void notifyBeforeEnterBackground(); void notifyEnterBackground(); + void notifyToggleAwayMode(); void performHapticFeedback(int duration); void processURL(const QString &url); @@ -55,7 +56,7 @@ signals: void enterForeground(); void beforeEnterBackground(); void enterBackground(); - + void toggleAwayMode(); void hapticFeedbackRequested(int duration); void handleSignupCompleted(); diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index b65f39ffd7..ca8883f660 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -9,6 +9,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // + #include "Application.h" #include @@ -37,7 +38,6 @@ #include #include - #include #include #include @@ -51,6 +51,7 @@ #include #include + #include #include #include @@ -191,6 +192,9 @@ #include "scripting/WalletScriptingInterface.h" #include "scripting/TTSScriptingInterface.h" #include "scripting/KeyboardScriptingInterface.h" + + + #if defined(Q_OS_MAC) || defined(Q_OS_WIN) #include "SpeechRecognizer.h" #endif @@ -239,6 +243,7 @@ #include "webbrowser/WebBrowserSuggestionsEngine.h" #include + #include "AboutUtil.h" #if defined(Q_OS_WIN) @@ -622,8 +627,6 @@ public: switch (type) { case NestableType::Entity: return getEntityModelProvider(static_cast(uuid)); - case NestableType::Overlay: - return nullptr; case NestableType::Avatar: return getAvatarModelProvider(uuid); } @@ -1058,6 +1061,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo connect(PluginManager::getInstance().data(), &PluginManager::inputDeviceRunningChanged, controllerScriptingInterface, &controller::ScriptingInterface::updateRunningInputDevices); + EntityTree::setEntityClicksCapturedOperator([this] { + return _controllerScriptingInterface->areEntityClicksCaptured(); + }); + _entityClipboard->createRootElement(); #ifdef Q_OS_WIN @@ -1072,6 +1079,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/FiraSans-SemiBold.ttf"); QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-Light.ttf"); QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-Regular.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/rawline-500.ttf"); QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-Bold.ttf"); QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-SemiBold.ttf"); QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Cairo-SemiBold.ttf"); @@ -1747,12 +1755,13 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo }); _applicationStateDevice->setInputVariant(STATE_PLATFORM_ANDROID, []() -> float { #if defined(Q_OS_ANDROID) - return 1; + return 1 ; #else return 0; #endif }); + // Setup the _keyboardMouseDevice, _touchscreenDevice, _touchscreenVirtualPadDevice and the user input mapper with the default bindings userInputMapper->registerDevice(_keyboardMouseDevice->getInputDevice()); // if the _touchscreenDevice is not supported it will not be registered @@ -1850,6 +1859,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo this->installEventFilter(this); + + #ifdef HAVE_DDE auto ddeTracker = DependencyManager::get(); ddeTracker->init(); @@ -1911,46 +1922,34 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo } }, Qt::QueuedConnection); - EntityTree::setAddMaterialToEntityOperator([this](const QUuid& entityID, graphics::MaterialLayer material, const std::string& parentMaterialName) { + EntityTreeRenderer::setAddMaterialToEntityOperator([this](const QUuid& entityID, graphics::MaterialLayer material, const std::string& parentMaterialName) { if (_aboutToQuit) { return false; } - // try to find the renderable auto renderable = getEntities()->renderableForEntityId(entityID); if (renderable) { renderable->addMaterial(material, parentMaterialName); - } - - // even if we don't find it, try to find the entity - auto entity = getEntities()->getEntity(entityID); - if (entity) { - entity->addMaterial(material, parentMaterialName); return true; } + return false; }); - EntityTree::setRemoveMaterialFromEntityOperator([this](const QUuid& entityID, graphics::MaterialPointer material, const std::string& parentMaterialName) { + EntityTreeRenderer::setRemoveMaterialFromEntityOperator([this](const QUuid& entityID, graphics::MaterialPointer material, const std::string& parentMaterialName) { if (_aboutToQuit) { return false; } - // try to find the renderable auto renderable = getEntities()->renderableForEntityId(entityID); if (renderable) { renderable->removeMaterial(material, parentMaterialName); - } - - // even if we don't find it, try to find the entity - auto entity = getEntities()->getEntity(entityID); - if (entity) { - entity->removeMaterial(material, parentMaterialName); return true; } + return false; }); - EntityTree::setAddMaterialToAvatarOperator([](const QUuid& avatarID, graphics::MaterialLayer material, const std::string& parentMaterialName) { + EntityTreeRenderer::setAddMaterialToAvatarOperator([](const QUuid& avatarID, graphics::MaterialLayer material, const std::string& parentMaterialName) { auto avatarManager = DependencyManager::get(); auto avatar = avatarManager->getAvatarBySessionID(avatarID); if (avatar) { @@ -1959,7 +1958,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo } return false; }); - EntityTree::setRemoveMaterialFromAvatarOperator([](const QUuid& avatarID, graphics::MaterialPointer material, const std::string& parentMaterialName) { + EntityTreeRenderer::setRemoveMaterialFromAvatarOperator([](const QUuid& avatarID, graphics::MaterialPointer material, const std::string& parentMaterialName) { auto avatarManager = DependencyManager::get(); auto avatar = avatarManager->getAvatarBySessionID(avatarID); if (avatar) { @@ -2282,7 +2281,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // Setup the mouse ray pick and related operators { - auto mouseRayPick = std::make_shared(Vectors::ZERO, Vectors::UP, PickFilter(PickScriptingInterface::PICK_ENTITIES()), 0.0f, true); + auto mouseRayPick = std::make_shared(Vectors::ZERO, Vectors::UP, PickFilter(PickScriptingInterface::PICK_ENTITIES() | PickScriptingInterface::PICK_LOCAL_ENTITIES()), 0.0f, true); mouseRayPick->parentTransform = std::make_shared(); mouseRayPick->setJointState(PickQuery::JOINT_STATE_MOUSE); auto mouseRayPickID = DependencyManager::get()->addPick(PickQuery::Ray, mouseRayPick); @@ -2308,31 +2307,31 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo DependencyManager::get()->setPrecisionPicking(rayPickID, value); }); - EntityItem::setBillboardRotationOperator([this](const glm::vec3& position, const glm::quat& rotation, BillboardMode billboardMode) { + EntityItem::setBillboardRotationOperator([this](const glm::vec3& position, const glm::quat& rotation, BillboardMode billboardMode, const glm::vec3& frustumPos) { if (billboardMode == BillboardMode::YAW) { //rotate about vertical to face the camera - ViewFrustum frustum; - copyViewFrustum(frustum); - glm::vec3 dPosition = frustum.getPosition() - position; + glm::vec3 dPosition = frustumPos - position; // If x and z are 0, atan(x, z) is undefined, so default to 0 degrees float yawRotation = dPosition.x == 0.0f && dPosition.z == 0.0f ? 0.0f : glm::atan(dPosition.x, dPosition.z); return glm::quat(glm::vec3(0.0f, yawRotation, 0.0f)); } else if (billboardMode == BillboardMode::FULL) { - ViewFrustum frustum; - copyViewFrustum(frustum); - glm::vec3 cameraPos = frustum.getPosition(); // use the referencial from the avatar, y isn't always up glm::vec3 avatarUP = DependencyManager::get()->getMyAvatar()->getWorldOrientation() * Vectors::UP; // check to see if glm::lookAt will work / using glm::lookAt variable name - glm::highp_vec3 s(glm::cross(position - cameraPos, avatarUP)); + glm::highp_vec3 s(glm::cross(position - frustumPos, avatarUP)); // make sure s is not NaN for any component if (glm::length2(s) > 0.0f) { - return glm::conjugate(glm::toQuat(glm::lookAt(cameraPos, position, avatarUP))); + return glm::conjugate(glm::toQuat(glm::lookAt(frustumPos, position, avatarUP))); } } return rotation; }); + EntityItem::setPrimaryViewFrustumPositionOperator([this]() { + ViewFrustum viewFrustum; + copyViewFrustum(viewFrustum); + return viewFrustum.getPosition(); + }); render::entities::WebEntityRenderer::setAcquireWebSurfaceOperator([this](const QString& url, bool htmlContent, QSharedPointer& webSurface, bool& cachedWebSurface) { bool isTablet = url == TabletScriptingInterface::QML; @@ -2347,6 +2346,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo } else { QObject::connect(webSurface.data(), &hifi::qml::OffscreenSurface::rootContextCreated, rootItemLoadedFunctor); } + auto surfaceContext = webSurface->getSurfaceContext(); + surfaceContext->setContextProperty("KeyboardScriptingInterface", DependencyManager::get().data()); } else { // FIXME: the tablet should use the OffscreenQmlSurfaceCache webSurface = QSharedPointer(new OffscreenQmlSurface(), [](OffscreenQmlSurface* webSurface) { @@ -2418,6 +2419,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo connect(&AndroidHelper::instance(), &AndroidHelper::beforeEnterBackground, this, &Application::beforeEnterBackground); connect(&AndroidHelper::instance(), &AndroidHelper::enterBackground, this, &Application::enterBackground); connect(&AndroidHelper::instance(), &AndroidHelper::enterForeground, this, &Application::enterForeground); + connect(&AndroidHelper::instance(), &AndroidHelper::toggleAwayMode, this, &Application::toggleAwayMode); AndroidHelper::instance().notifyLoadComplete(); #endif pauseUntilLoginDetermined(); @@ -3099,7 +3101,7 @@ void Application::initializeUi() { } if (TouchscreenVirtualPadDevice::NAME == inputPlugin->getName()) { _touchscreenVirtualPadDevice = std::dynamic_pointer_cast(inputPlugin); -#if defined(Q_OS_ANDROID) +#if defined(ANDROID_APP_INTERFACE) auto& virtualPadManager = VirtualPad::Manager::instance(); connect(&virtualPadManager, &VirtualPad::Manager::hapticFeedbackRequested, this, [](int duration) { @@ -3282,6 +3284,7 @@ void Application::setupQmlSurface(QQmlContext* surfaceContext, bool setAdditiona surfaceContext->setContextProperty("MyAvatar", DependencyManager::get()->getMyAvatar().get()); surfaceContext->setContextProperty("Entities", DependencyManager::get().data()); surfaceContext->setContextProperty("Snapshot", DependencyManager::get().data()); + surfaceContext->setContextProperty("KeyboardScriptingInterface", DependencyManager::get().data()); if (setAdditionalContextProperties) { auto tabletScriptingInterface = DependencyManager::get(); @@ -3292,7 +3295,6 @@ void Application::setupQmlSurface(QQmlContext* surfaceContext, bool setAdditiona surfaceContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); surfaceContext->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); - surfaceContext->setContextProperty("KeyboardScriptingInterface", DependencyManager::get().data()); surfaceContext->setContextProperty("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED surfaceContext->setContextProperty("GlobalServices", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED @@ -3631,10 +3633,14 @@ void Application::handleSandboxStatus(QNetworkReply* reply) { } // Get controller availability +#ifdef ANDROID_APP_QUEST_INTERFACE + bool hasHandControllers = true; +#else bool hasHandControllers = false; if (PluginUtils::isViveControllerAvailable() || PluginUtils::isOculusTouchControllerAvailable()) { hasHandControllers = true; } +#endif // Check HMD use (may be technically available without being in use) bool hasHMD = PluginUtils::isHMDAvailable(); @@ -3664,8 +3670,8 @@ void Application::handleSandboxStatus(QNetworkReply* reply) { // If this is a first run we short-circuit the address passed in if (_firstRun.get()) { #if !defined(Q_OS_ANDROID) - DependencyManager::get()->goToEntry(); - sentTo = SENT_TO_ENTRY; + DependencyManager::get()->goToEntry(); + sentTo = SENT_TO_ENTRY; #endif _firstRun.set(false); @@ -3776,9 +3782,12 @@ std::map Application::prepareServerlessDomainContents(QUrl dom tmpTree->reaverageOctreeElements(); tmpTree->sendEntities(&_entityEditSender, getEntities()->getTree(), 0, 0, 0); } + std::map namedPaths = tmpTree->getNamedPaths(); - return tmpTree->getNamedPaths(); + // we must manually eraseAllOctreeElements(false) else the tmpTree will mem-leak + tmpTree->eraseAllOctreeElements(false); + return namedPaths; } void Application::loadServerlessDomain(QUrl domainURL) { @@ -4389,7 +4398,6 @@ void Application::mouseMoveEvent(QMouseEvent* event) { if (compositor.getReticleVisible() || !isHMDMode() || !compositor.getReticleOverDesktop() || getOverlays().getOverlayAtPoint(glm::vec2(transformedPos.x(), transformedPos.y())) != UNKNOWN_ENTITY_ID) { getEntities()->mouseMoveEvent(&mappedEvent); - getOverlays().mouseMoveEvent(&mappedEvent); } _controllerScriptingInterface->emitMouseMoveEvent(&mappedEvent); // send events to any registered scripts @@ -4423,14 +4431,8 @@ void Application::mousePressEvent(QMouseEvent* event) { #endif QMouseEvent mappedEvent(event->type(), transformedPos, event->screenPos(), event->button(), event->buttons(), event->modifiers()); - std::pair entityResult; - if (!_controllerScriptingInterface->areEntityClicksCaptured()) { - entityResult = getEntities()->mousePressEvent(&mappedEvent); - } - std::pair overlayResult = getOverlays().mousePressEvent(&mappedEvent); - - QUuid focusedEntity = entityResult.first < overlayResult.first ? entityResult.second : overlayResult.second; - setKeyboardFocusEntity(getEntities()->wantsKeyboardFocus(focusedEntity) ? focusedEntity : UNKNOWN_ENTITY_ID); + QUuid result = getEntities()->mousePressEvent(&mappedEvent); + setKeyboardFocusEntity(getEntities()->wantsKeyboardFocus(result) ? result : UNKNOWN_ENTITY_ID); _controllerScriptingInterface->emitMousePressEvent(&mappedEvent); // send events to any registered scripts @@ -4469,11 +4471,7 @@ void Application::mouseDoublePressEvent(QMouseEvent* event) { transformedPos, event->screenPos(), event->button(), event->buttons(), event->modifiers()); - - if (!_controllerScriptingInterface->areEntityClicksCaptured()) { - getEntities()->mouseDoublePressEvent(&mappedEvent); - } - getOverlays().mouseDoublePressEvent(&mappedEvent); + getEntities()->mouseDoublePressEvent(&mappedEvent); // if one of our scripts have asked to capture this event, then stop processing it if (_controllerScriptingInterface->isMouseCaptured()) { @@ -4498,7 +4496,6 @@ void Application::mouseReleaseEvent(QMouseEvent* event) { event->buttons(), event->modifiers()); getEntities()->mouseReleaseEvent(&mappedEvent); - getOverlays().mouseReleaseEvent(&mappedEvent); _controllerScriptingInterface->emitMouseReleaseEvent(&mappedEvent); // send events to any registered scripts @@ -4982,6 +4979,15 @@ void Application::idle() { } } + { + if (_keyboardFocusWaitingOnRenderable && getEntities()->renderableForEntityId(_keyboardFocusedEntity.get())) { + QUuid entityId = _keyboardFocusedEntity.get(); + setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); + _keyboardFocusWaitingOnRenderable = false; + setKeyboardFocusEntity(entityId); + } + } + { PerformanceTimer perfTimer("pluginIdle"); PerformanceWarning warn(showWarnings, "Application::idle()... pluginIdle()"); @@ -5764,6 +5770,11 @@ void Application::reloadResourceCaches() { DependencyManager::get()->reset(); // Force redownload of .fst models + DependencyManager::get()->reloadAllScripts(); + getOffscreenUI()->clearCache(); + + DependencyManager::get()->createKeyboard(); + getMyAvatar()->resetFullAvatarURL(); } @@ -5810,7 +5821,7 @@ void Application::setKeyboardFocusEntity(const QUuid& id) { if (qApp->getLoginDialogPoppedUp() && !_loginDialogID.isNull()) { if (id == _loginDialogID) { emit loginDialogFocusEnabled(); - } else { + } else if (!_keyboardFocusWaitingOnRenderable) { // that's the only entity we want in focus; return; } @@ -5827,7 +5838,10 @@ void Application::setKeyboardFocusEntity(const QUuid& id) { if (properties.getVisible()) { auto entities = getEntities(); auto entityId = _keyboardFocusedEntity.get(); - if (entities->wantsKeyboardFocus(entityId)) { + auto entityItemRenderable = entities->renderableForEntityId(entityId); + if (!entityItemRenderable) { + _keyboardFocusWaitingOnRenderable = true; + } else if (entityItemRenderable->wantsKeyboardFocus()) { entities->setProxyWindow(entityId, _window->windowHandle()); if (_keyboardMouseDevice->isActive()) { _keyboardMouseDevice->pluginFocusOutEvent(); @@ -6931,7 +6945,7 @@ void Application::clearDomainOctreeDetails(bool clearAll) { }); // reset the model renderer - clearAll ? getEntities()->clear() : getEntities()->clearNonLocalEntities(); + clearAll ? getEntities()->clear() : getEntities()->clearDomainAndNonOwnedEntities(); auto skyStage = DependencyManager::get()->getSkyStage(); @@ -8251,6 +8265,7 @@ void Application::loadDomainConnectionDialog() { } void Application::toggleLogDialog() { +#ifndef ANDROID_APP_QUEST_INTERFACE if (getLoginDialogPoppedUp()) { return; } @@ -8274,6 +8289,7 @@ void Application::toggleLogDialog() { } else { _logDialog->show(); } +#endif } void Application::recreateLogWindow(int keepOnTop) { @@ -9136,23 +9152,40 @@ void Application::beforeEnterBackground() { clearDomainOctreeDetails(); } + + void Application::enterBackground() { QMetaObject::invokeMethod(DependencyManager::get().data(), "stop", Qt::BlockingQueuedConnection); +// Quest only supports one plugin which can't be deactivated currently +#if !defined(ANDROID_APP_QUEST_INTERFACE) if (getActiveDisplayPlugin()->isActive()) { getActiveDisplayPlugin()->deactivate(); } +#endif } void Application::enterForeground() { QMetaObject::invokeMethod(DependencyManager::get().data(), "start", Qt::BlockingQueuedConnection); +// Quest only supports one plugin which can't be deactivated currently +#if !defined(ANDROID_APP_QUEST_INTERFACE) if (!getActiveDisplayPlugin() || getActiveDisplayPlugin()->isActive() || !getActiveDisplayPlugin()->activate()) { qWarning() << "Could not re-activate display plugin"; } +#endif auto nodeList = DependencyManager::get(); nodeList->setSendDomainServerCheckInEnabled(true); } + + +void Application::toggleAwayMode(){ + QKeyEvent event = QKeyEvent (QEvent::KeyPress, Qt::Key_Escape, Qt::NoModifier); + QCoreApplication::sendEvent (this, &event); +} + + #endif + #include "Application.moc" diff --git a/interface/src/Application.h b/interface/src/Application.h index afd9f5f12f..a8cc9450c5 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -338,7 +338,8 @@ public: void beforeEnterBackground(); void enterBackground(); void enterForeground(); -#endif + void toggleAwayMode(); + #endif signals: void svoImportRequested(const QString& url); @@ -732,6 +733,7 @@ private: bool _failedToConnectToEntityServer { false }; bool _reticleClickPressed { false }; + bool _keyboardFocusWaitingOnRenderable { false }; int _avatarAttachmentRequest = 0; diff --git a/interface/src/LODManager.h b/interface/src/LODManager.h index 559bae1779..77cb1a0d39 100644 --- a/interface/src/LODManager.h +++ b/interface/src/LODManager.h @@ -19,13 +19,19 @@ #include #include + #ifdef Q_OS_ANDROID -const float LOD_DEFAULT_QUALITY_LEVEL = 0.75f; // default quality level setting is High (lower framerate) +const float LOD_DEFAULT_QUALITY_LEVEL = 0.2f; // default quality level setting is High (lower framerate) #else const float LOD_DEFAULT_QUALITY_LEVEL = 0.5f; // default quality level setting is Mid #endif const float LOD_MAX_LIKELY_DESKTOP_FPS = 60.0f; // this is essentially, V-synch fps +#ifdef Q_OS_ANDROID +const float LOD_MAX_LIKELY_HMD_FPS = 36.0f; // this is essentially, V-synch fps +#else const float LOD_MAX_LIKELY_HMD_FPS = 90.0f; // this is essentially, V-synch fps +#endif + const float LOD_OFFSET_FPS = 5.0f; // offset of FPS to add for computing the target framerate class AABox; diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 0b33220c01..55025b3b23 100755 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -297,6 +297,9 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { avatar->setIsNewAvatar(false); } avatar->simulate(deltaTime, inView); + if (avatar->getSkeletonModel()->isLoaded() && avatar->getWorkloadRegion() == workload::Region::R1) { + _myAvatar->addAvatarHandsToFlow(avatar); + } avatar->updateRenderItem(renderTransaction); avatar->updateSpaceProxy(workloadTransaction); avatar->setLastRenderUpdateTime(startTime); @@ -716,7 +719,7 @@ RayToAvatarIntersectionResult AvatarManager::findRayIntersectionVector(const Pic } } - if (rayAvatarResult._intersect && pickAgainstMesh) { + if (avatar && rayAvatarResult._intersect && pickAgainstMesh) { glm::vec3 localRayOrigin = avatar->worldToJointPoint(ray.origin, rayAvatarResult._intersectWithJoint); glm::vec3 localRayPoint = avatar->worldToJointPoint(ray.origin + rayAvatarResult._distance * rayDirection, rayAvatarResult._intersectWithJoint); diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index b020cdb627..260ff33db7 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -254,11 +254,15 @@ void AvatarProject::openInInventory() const { DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system")); tablet->loadQMLSource("hifi/commerce/wallet/Wallet.qml"); DependencyManager::get()->openTablet(); - tablet->getTabletRoot()->forceActiveFocus(); - auto name = getProjectName(); // I'm not a fan of this, but it's the only current option. + auto name = getProjectName(); QTimer::singleShot(TIME_TO_WAIT_FOR_INVENTORY_TO_OPEN_MS, [name, tablet]() { tablet->sendToQml(QVariantMap({ { "method", "updatePurchases" }, { "filterText", name } })); }); + + QQuickItem* root = tablet->getTabletRoot(); + if (root) { + root->forceActiveFocus(); + } } diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index f11547bdca..0a63290051 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -77,7 +77,10 @@ public: return QDir::cleanPath(QDir(_projectPath).absoluteFilePath(_fst->getModelPath())); } Q_INVOKABLE bool getHasErrors() const { return _hasErrors; } - Q_INVOKABLE void setHasErrors(bool hasErrors) { _hasErrors = hasErrors; } + Q_INVOKABLE void setHasErrors(bool hasErrors) { + _hasErrors = hasErrors; + emit hasErrorsChanged(); + } /** * returns the AvatarProject or a nullptr on failure. diff --git a/interface/src/avatar/GrabManager.cpp b/interface/src/avatar/GrabManager.cpp index db1337b64d..8273def490 100644 --- a/interface/src/avatar/GrabManager.cpp +++ b/interface/src/avatar/GrabManager.cpp @@ -32,6 +32,17 @@ void GrabManager::simulateGrabs() { bool success; SpatiallyNestablePointer grabbedThing = SpatiallyNestable::findByID(grabbedThingID, success); if (success && grabbedThing) { + auto entity = std::dynamic_pointer_cast(grabbedThing); + if (entity) { + if (entity->getLocked()) { + continue; // even if someone else claims to be grabbing it, don't move a locked thing + } + const GrabPropertyGroup& grabProps = entity->getGrabProperties(); + if (!grabProps.getGrabbable()) { + continue; // even if someone else claims to be grabbing it, don't move non-grabbable + } + } + glm::vec3 finalPosition = acc.finalizePosition(); glm::quat finalOrientation = acc.finalizeOrientation(); grabbedThing->setTransform(createMatFromQuatAndPos(finalOrientation, finalPosition)); diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp old mode 100755 new mode 100644 index f62896772d..3db1228796 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -910,7 +910,7 @@ void MyAvatar::simulate(float deltaTime, bool inView) { recorder->recordFrame(FRAME_TYPE, toFrame(*this)); } - locationChanged(); + locationChanged(true, false); // if a entity-child of this avatar has moved outside of its queryAACube, update the cube and tell the entity server. auto entityTreeRenderer = qApp->getEntities(); EntityTreePointer entityTree = entityTreeRenderer ? entityTreeRenderer->getTree() : nullptr; @@ -920,6 +920,7 @@ void MyAvatar::simulate(float deltaTime, bool inView) { zoneInteractionProperties = entityTreeRenderer->getZoneInteractionProperties(); EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender(); forEachDescendant([&](SpatiallyNestablePointer object) { + locationChanged(true, false); // we need to update attached queryAACubes in our own local tree so point-select always works // however we don't want to flood the update pipeline with AvatarEntity updates, so we assume // others have all info required to properly update queryAACube of AvatarEntities on their end @@ -932,7 +933,8 @@ void MyAvatar::simulate(float deltaTime, bool inView) { bool isPhysicsEnabled = qApp->isPhysicsEnabled(); bool zoneAllowsFlying = zoneInteractionProperties.first; bool collisionlessAllowed = zoneInteractionProperties.second; - _characterController.setFlyingAllowed((zoneAllowsFlying && _enableFlying) || !isPhysicsEnabled); + _characterController.setZoneFlyingAllowed(zoneAllowsFlying || !isPhysicsEnabled); + _characterController.setComfortFlyingAllowed(_enableFlying); _characterController.setCollisionlessAllowed(collisionlessAllowed); } @@ -2323,23 +2325,25 @@ void MyAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { std::shared_ptr skeletonConnection = std::make_shared(); *skeletonConnection = QObject::connect(_skeletonModel.get(), &SkeletonModel::skeletonLoaded, [this, skeletonModelChangeCount, skeletonConnection]() { - if (skeletonModelChangeCount == _skeletonModelChangeCount) { + if (skeletonModelChangeCount == _skeletonModelChangeCount) { - if (_fullAvatarModelName.isEmpty()) { - // Store the FST file name into preferences - const auto& mapping = _skeletonModel->getGeometry()->getMapping(); - if (mapping.value("name").isValid()) { - _fullAvatarModelName = mapping.value("name").toString(); - } - } + if (_fullAvatarModelName.isEmpty()) { + // Store the FST file name into preferences + const auto& mapping = _skeletonModel->getGeometry()->getMapping(); + if (mapping.value("name").isValid()) { + _fullAvatarModelName = mapping.value("name").toString(); + } + } - initHeadBones(); - _skeletonModel->setCauterizeBoneSet(_headBoneSet); - _fstAnimGraphOverrideUrl = _skeletonModel->getGeometry()->getAnimGraphOverrideUrl(); - initAnimGraph(); - _skeletonModelLoaded = true; - } - QObject::disconnect(*skeletonConnection); + initHeadBones(); + _skeletonModel->setCauterizeBoneSet(_headBoneSet); + _fstAnimGraphOverrideUrl = _skeletonModel->getGeometry()->getAnimGraphOverrideUrl(); + initAnimGraph(); + initFlowFromFST(); + + _skeletonModelLoaded = true; + } + QObject::disconnect(*skeletonConnection); }); saveAvatarUrl(); @@ -2963,6 +2967,10 @@ void MyAvatar::initAnimGraph() { graphUrl = _fstAnimGraphOverrideUrl; } else { graphUrl = PathUtils::resourcesUrl("avatar/avatar-animation.json"); + +#if defined(Q_OS_ANDROID) || defined(HIFI_USE_OPTIMIZED_IK) + graphUrl = PathUtils::resourcesUrl("avatar/avatar-animation_withSplineIKNode.json"); +#endif } emit animGraphUrlChanged(graphUrl); @@ -5339,6 +5347,81 @@ void MyAvatar::releaseGrab(const QUuid& grabID) { } } +void MyAvatar::addAvatarHandsToFlow(const std::shared_ptr& otherAvatar) { + auto &flow = _skeletonModel->getRig().getFlow(); + for (auto &handJointName : HAND_COLLISION_JOINTS) { + int jointIndex = otherAvatar->getJointIndex(handJointName); + if (jointIndex != -1) { + glm::vec3 position = otherAvatar->getJointPosition(jointIndex); + flow.setOthersCollision(otherAvatar->getID(), jointIndex, position); + } + } +} + +void MyAvatar::useFlow(bool isActive, bool isCollidable, const QVariantMap& physicsConfig, const QVariantMap& collisionsConfig) { + if (_skeletonModel->isLoaded()) { + _skeletonModel->getRig().initFlow(isActive); + auto &flow = _skeletonModel->getRig().getFlow(); + auto &collisionSystem = flow.getCollisionSystem(); + collisionSystem.setActive(isCollidable); + auto physicsGroups = physicsConfig.keys(); + if (physicsGroups.size() > 0) { + for (auto &groupName : physicsGroups) { + auto settings = physicsConfig[groupName].toMap(); + FlowPhysicsSettings physicsSettings; + if (settings.contains("active")) { + physicsSettings._active = settings["active"].toBool(); + } + if (settings.contains("damping")) { + physicsSettings._damping = settings["damping"].toFloat(); + } + if (settings.contains("delta")) { + physicsSettings._delta = settings["delta"].toFloat(); + } + if (settings.contains("gravity")) { + physicsSettings._gravity = settings["gravity"].toFloat(); + } + if (settings.contains("inertia")) { + physicsSettings._inertia = settings["inertia"].toFloat(); + } + if (settings.contains("radius")) { + physicsSettings._radius = settings["radius"].toFloat(); + } + if (settings.contains("stiffness")) { + physicsSettings._stiffness = settings["stiffness"].toFloat(); + } + flow.setPhysicsSettingsForGroup(groupName, physicsSettings); + } + } + auto collisionJoints = collisionsConfig.keys(); + if (collisionJoints.size() > 0) { + collisionSystem.resetCollisions(); + for (auto &jointName : collisionJoints) { + int jointIndex = getJointIndex(jointName); + FlowCollisionSettings collisionsSettings; + auto settings = collisionsConfig[jointName].toMap(); + collisionsSettings._entityID = getID(); + if (settings.contains("radius")) { + collisionsSettings._radius = settings["radius"].toFloat(); + } + if (settings.contains("offset")) { + collisionsSettings._offset = vec3FromVariant(settings["offset"]); + } + collisionSystem.addCollisionSphere(jointIndex, collisionsSettings); + } + } + } +} + +void MyAvatar::initFlowFromFST() { + if (_skeletonModel->isLoaded()) { + auto &flowData = _skeletonModel->getHFMModel().flowData; + if (flowData.shouldInitFlow()) { + useFlow(true, flowData.shouldInitCollisions(), flowData._physicsConfig, flowData._collisionsConfig); + } + } +} + void MyAvatar::sendPacket(const QUuid& entityID, const EntityItemProperties& properties) const { auto treeRenderer = DependencyManager::get(); EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index f05bdd5184..5e049c7a02 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1544,6 +1544,20 @@ public: * @comment Uses the base class's JSDoc. */ int sendAvatarDataPacket(bool sendAll = false) override; + + void addAvatarHandsToFlow(const std::shared_ptr& otherAvatar); + + /**jsdoc + * Init flow simulation on avatar. + * @function MyAvatar.useFlow + * @param {boolean} - Set to true to activate flow simulation. + * @param {boolean} - Set to true to activate collisions. + * @param {Object} physicsConfig - object with the customized physic parameters + * i.e. {"hair": {"active": true, "stiffness": 0.0, "radius": 0.04, "gravity": -0.035, "damping": 0.8, "inertia": 0.8, "delta": 0.35}} + * @param {Object} collisionsConfig - object with the customized collision parameters + * i.e. {"Spine2": {"type": "sphere", "radius": 0.14, "offset": {"x": 0.0, "y": 0.2, "z": 0.0}}} + */ + Q_INVOKABLE void useFlow(bool isActive, bool isCollidable, const QVariantMap& physicsConfig = QVariantMap(), const QVariantMap& collisionsConfig = QVariantMap()); public slots: @@ -2201,6 +2215,7 @@ private: void updateCollisionSound(const glm::vec3& penetration, float deltaTime, float frequency); void initHeadBones(); void initAnimGraph(); + void initFlowFromFST(); // Avatar Preferences QUrl _fullAvatarURLFromPreferences; diff --git a/interface/src/avatar/MySkeletonModel.cpp b/interface/src/avatar/MySkeletonModel.cpp index 26d69841d0..55c29b66c1 100755 --- a/interface/src/avatar/MySkeletonModel.cpp +++ b/interface/src/avatar/MySkeletonModel.cpp @@ -10,12 +10,14 @@ #include #include +#include #include "Application.h" #include "InterfaceLogging.h" #include "AnimUtil.h" + MySkeletonModel::MySkeletonModel(Avatar* owningAvatar, QObject* parent) : SkeletonModel(owningAvatar, parent) { } @@ -33,6 +35,22 @@ Rig::CharacterControllerState convertCharacterControllerState(CharacterControlle }; } +#if defined(Q_OS_ANDROID) || defined(HIFI_USE_OPTIMIZED_IK) +static glm::vec3 computeSpine2WithHeadHipsSpline(MyAvatar* myAvatar, AnimPose hipsIKTargetPose, AnimPose headIKTargetPose) { + + // the the ik targets to compute the spline with + CubicHermiteSplineFunctorWithArcLength splineFinal(headIKTargetPose.rot(), headIKTargetPose.trans(), hipsIKTargetPose.rot(), hipsIKTargetPose.trans()); + + // measure the total arc length along the spline + float totalArcLength = splineFinal.arcLength(1.0f); + float tFinal = splineFinal.arcLengthInverse(myAvatar->getSpine2SplineRatio() * totalArcLength); + glm::vec3 spine2Translation = splineFinal(tFinal); + + return spine2Translation + myAvatar->getSpine2SplineOffset(); + +} +#endif + static AnimPose computeHipsInSensorFrame(MyAvatar* myAvatar, bool isFlying) { glm::mat4 worldToSensorMat = glm::inverse(myAvatar->getSensorToWorldMatrix()); @@ -233,6 +251,12 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { myAvatar->getControllerPoseInAvatarFrame(controller::Action::LEFT_HAND).isValid() && !(params.primaryControllerFlags[Rig::PrimaryControllerType_Spine2] & (uint8_t)Rig::ControllerFlags::Enabled)) { +#if defined(Q_OS_ANDROID) || defined(HIFI_USE_OPTIMIZED_IK) + AnimPose headAvatarSpace(avatarHeadPose.getRotation(), avatarHeadPose.getTranslation()); + AnimPose headRigSpace = avatarToRigPose * headAvatarSpace; + AnimPose hipsRigSpace = sensorToRigPose * sensorHips; + glm::vec3 spine2TargetTranslation = computeSpine2WithHeadHipsSpline(myAvatar, hipsRigSpace, headRigSpace); +#endif const float SPINE2_ROTATION_FILTER = 0.5f; AnimPose currentSpine2Pose; AnimPose currentHeadPose; @@ -243,6 +267,9 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { if (spine2Exists && headExists && hipsExists) { AnimPose rigSpaceYaw(myAvatar->getSpine2RotationRigSpace()); +#if defined(Q_OS_ANDROID) || defined(HIFI_USE_OPTIMIZED_IK) + rigSpaceYaw.rot() = safeLerp(Quaternions::IDENTITY, rigSpaceYaw.rot(), 0.5f); +#endif glm::vec3 u, v, w; glm::vec3 fwd = rigSpaceYaw.rot() * glm::vec3(0.0f, 0.0f, 1.0f); glm::vec3 up = currentHeadPose.trans() - currentHipsPose.trans(); @@ -253,6 +280,9 @@ void MySkeletonModel::updateRig(float deltaTime, glm::mat4 parentTransform) { } generateBasisVectors(up, fwd, u, v, w); AnimPose newSpinePose(glm::mat4(glm::vec4(w, 0.0f), glm::vec4(u, 0.0f), glm::vec4(v, 0.0f), glm::vec4(glm::vec3(0.0f, 0.0f, 0.0f), 1.0f))); +#if defined(Q_OS_ANDROID) || defined(HIFI_USE_OPTIMIZED_IK) + currentSpine2Pose.trans() = spine2TargetTranslation; +#endif currentSpine2Pose.rot() = safeLerp(currentSpine2Pose.rot(), newSpinePose.rot(), SPINE2_ROTATION_FILTER); params.primaryControllerPoses[Rig::PrimaryControllerType_Spine2] = currentSpine2Pose; params.primaryControllerFlags[Rig::PrimaryControllerType_Spine2] = (uint8_t)Rig::ControllerFlags::Enabled | (uint8_t)Rig::ControllerFlags::Estimated; diff --git a/interface/src/avatar/OtherAvatar.cpp b/interface/src/avatar/OtherAvatar.cpp index 40c7c01b30..7848c46eee 100755 --- a/interface/src/avatar/OtherAvatar.cpp +++ b/interface/src/avatar/OtherAvatar.cpp @@ -368,7 +368,6 @@ void OtherAvatar::simulate(float deltaTime, bool inView) { PROFILE_RANGE(simulation, "grabs"); applyGrabChanges(); } - updateFadingStatus(); } @@ -504,6 +503,7 @@ void OtherAvatar::handleChangedAvatarEntityData() { // then set the the original ID for the changes to take effect // TODO: This is a horrible hack and once properties.constructFromBuffer no longer causes // side effects...remove the following three lines + const QUuid NULL_ID = QUuid("{00000000-0000-0000-0000-000000000005}"); entity->setParentID(NULL_ID); entity->setParentID(oldParentID); diff --git a/interface/src/avatar/OtherAvatar.h b/interface/src/avatar/OtherAvatar.h index 696e122b30..7669f44806 100644 --- a/interface/src/avatar/OtherAvatar.h +++ b/interface/src/avatar/OtherAvatar.h @@ -48,6 +48,7 @@ public: void rebuildCollisionShape() override; void setWorkloadRegion(uint8_t region); + uint8_t getWorkloadRegion() { return _workloadRegion; } bool shouldBeInPhysicsSimulation() const; bool needsPhysicsUpdate() const; diff --git a/interface/src/commerce/Wallet.h b/interface/src/commerce/Wallet.h index 5e5e6c9b4f..fdd6b5e2a6 100644 --- a/interface/src/commerce/Wallet.h +++ b/interface/src/commerce/Wallet.h @@ -62,8 +62,8 @@ public: * ValueMeaningDescription * * - * 0Not logged inThe user isn't logged in. - * 1Not set upThe user's wallet isn't set up. + * 0Not logged inThe user is not logged in. + * 1Not set upThe user's wallet has not been set up. * 2Pre-existingThere is a wallet present on the server but not one * locally. * 3ConflictingThere is a wallet present on the server plus one present locally, @@ -73,8 +73,8 @@ public: * 5ReadyThe wallet is ready for use. * * - *

Wallets used to be stored locally but now they're stored on the server, unless the computer once had a wallet stored - * locally in which case the wallet may be present in both places.

+ *

Wallets used to be stored locally but now they're only stored on the server. A wallet is present in both places if + * your computer previously stored its information locally.

* @typedef {number} WalletScriptingInterface.WalletStatus */ enum WalletStatus { diff --git a/interface/src/octree/SafeLanding.cpp b/interface/src/octree/SafeLanding.cpp index 9efad22d09..e56ca984e0 100644 --- a/interface/src/octree/SafeLanding.cpp +++ b/interface/src/octree/SafeLanding.cpp @@ -72,7 +72,7 @@ void SafeLanding::addTrackedEntity(const EntityItemID& entityID) { Locker lock(_lock); EntityItemPointer entity = _entityTree->findEntityByID(entityID); - if (entity && entity->getCreated() < _startTime) { + if (entity && !entity->isLocalEntity() && entity->getCreated() < _startTime) { _trackedEntities.emplace(entityID, entity); int trackedEntityCount = (int)_trackedEntities.size(); @@ -81,7 +81,7 @@ void SafeLanding::addTrackedEntity(const EntityItemID& entityID) { _maxTrackedEntityCount = trackedEntityCount; _trackedEntityStabilityCount = 0; } - qCDebug(interfaceapp) << "Safe Landing: Tracking entity " << entity->getItemName(); + //qCDebug(interfaceapp) << "Safe Landing: Tracking entity " << entity->getItemName(); } } } diff --git a/interface/src/raypick/LaserPointer.cpp b/interface/src/raypick/LaserPointer.cpp index aeed65fbad..bd746c9090 100644 --- a/interface/src/raypick/LaserPointer.cpp +++ b/interface/src/raypick/LaserPointer.cpp @@ -172,7 +172,10 @@ void LaserPointer::RenderState::update(const glm::vec3& origin, const glm::vec3& properties.setVisible(true); properties.setIgnorePickIntersection(doesPathIgnorePicks()); QVector widths; - widths.append(getLineWidth() * parentScale); + float width = getLineWidth() * parentScale; + widths.append(width); + widths.append(width); + properties.setStrokeWidths(widths); DependencyManager::get()->editEntity(getPathID(), properties); } } diff --git a/interface/src/raypick/PickScriptingInterface.cpp b/interface/src/raypick/PickScriptingInterface.cpp index ce01db3a56..09f4b68cb9 100644 --- a/interface/src/raypick/PickScriptingInterface.cpp +++ b/interface/src/raypick/PickScriptingInterface.cpp @@ -71,6 +71,18 @@ PickFilter getPickFilter(unsigned int filter) { unsigned int PickScriptingInterface::createRayPick(const QVariant& properties) { QVariantMap propMap = properties.toMap(); + +#if defined (Q_OS_ANDROID) + QString jointName { "" }; + if (propMap["joint"].isValid()) { + QString jointName = propMap["joint"].toString(); + const QString MOUSE_JOINT = "Mouse"; + if (jointName == MOUSE_JOINT) { + return PointerEvent::INVALID_POINTER_ID; + } + } +#endif + bool enabled = false; if (propMap["enabled"].isValid()) { enabled = propMap["enabled"].toBool(); diff --git a/interface/src/raypick/PointerScriptingInterface.cpp b/interface/src/raypick/PointerScriptingInterface.cpp index 19c20f0c06..1c80caff88 100644 --- a/interface/src/raypick/PointerScriptingInterface.cpp +++ b/interface/src/raypick/PointerScriptingInterface.cpp @@ -150,6 +150,17 @@ unsigned int PointerScriptingInterface::createStylus(const QVariant& properties) unsigned int PointerScriptingInterface::createLaserPointer(const QVariant& properties) const { QVariantMap propertyMap = properties.toMap(); +#if defined (Q_OS_ANDROID) + QString jointName { "" }; + if (propertyMap["joint"].isValid()) { + QString jointName = propertyMap["joint"].toString(); + const QString MOUSE_JOINT = "Mouse"; + if (jointName == MOUSE_JOINT) { + return PointerEvent::INVALID_POINTER_ID; + } + } +#endif + bool faceAvatar = false; if (propertyMap["faceAvatar"].isValid()) { faceAvatar = propertyMap["faceAvatar"].toBool(); diff --git a/interface/src/raypick/RayPick.cpp b/interface/src/raypick/RayPick.cpp index b030a67e17..8157c32a93 100644 --- a/interface/src/raypick/RayPick.cpp +++ b/interface/src/raypick/RayPick.cpp @@ -89,6 +89,8 @@ glm::vec3 RayPick::intersectRayWithEntityXYPlane(const QUuid& entityID, const gl return intersectRayWithXYPlane(origin, direction, props.getPosition(), props.getRotation(), props.getRegistrationPoint()); } + + glm::vec2 RayPick::projectOntoXYPlane(const glm::vec3& worldPos, const glm::vec3& position, const glm::quat& rotation, const glm::vec3& dimensions, const glm::vec3& registrationPoint, bool unNormalized) { glm::quat invRot = glm::inverse(rotation); glm::vec3 localPos = invRot * (worldPos - position); @@ -102,6 +104,19 @@ glm::vec2 RayPick::projectOntoXYPlane(const glm::vec3& worldPos, const glm::vec3 return pos2D; } +glm::vec2 RayPick::projectOntoXZPlane(const glm::vec3& worldPos, const glm::vec3& position, const glm::quat& rotation, const glm::vec3& dimensions, const glm::vec3& registrationPoint, bool unNormalized) { + glm::quat invRot = glm::inverse(rotation); + glm::vec3 localPos = invRot * (worldPos - position); + + glm::vec3 normalizedPos = (localPos / dimensions) + registrationPoint; + + glm::vec2 pos2D = glm::vec2(normalizedPos.x, (1.0f - normalizedPos.z)); + if (unNormalized) { + pos2D *= glm::vec2(dimensions.x, dimensions.z); + } + return pos2D; +} + glm::vec2 RayPick::projectOntoEntityXYPlane(const QUuid& entityID, const glm::vec3& worldPos, bool unNormalized) { EntityPropertyFlags desiredProperties; desiredProperties += PROP_POSITION; diff --git a/interface/src/raypick/RayPick.h b/interface/src/raypick/RayPick.h index a781795e55..161c930fef 100644 --- a/interface/src/raypick/RayPick.h +++ b/interface/src/raypick/RayPick.h @@ -85,9 +85,11 @@ public: static glm::vec3 intersectRayWithEntityXYPlane(const QUuid& entityID, const glm::vec3& origin, const glm::vec3& direction); static glm::vec2 projectOntoEntityXYPlane(const QUuid& entityID, const glm::vec3& worldPos, bool unNormalized = true); + static glm::vec2 projectOntoXYPlane(const glm::vec3& worldPos, const glm::vec3& position, const glm::quat& rotation, const glm::vec3& dimensions, const glm::vec3& registrationPoint, bool unNormalized); + static glm::vec2 projectOntoXZPlane(const glm::vec3& worldPos, const glm::vec3& position, const glm::quat& rotation, const glm::vec3& dimensions, const glm::vec3& registrationPoint, bool unNoemalized); + private: static glm::vec3 intersectRayWithXYPlane(const glm::vec3& origin, const glm::vec3& direction, const glm::vec3& point, const glm::quat& rotation, const glm::vec3& registration); - static glm::vec2 projectOntoXYPlane(const glm::vec3& worldPos, const glm::vec3& position, const glm::quat& rotation, const glm::vec3& dimensions, const glm::vec3& registrationPoint, bool unNormalized); }; #endif // hifi_RayPick_h diff --git a/interface/src/raypick/StylusPick.cpp b/interface/src/raypick/StylusPick.cpp index 0e95959566..9139a16c6d 100644 --- a/interface/src/raypick/StylusPick.cpp +++ b/interface/src/raypick/StylusPick.cpp @@ -137,13 +137,14 @@ PickResultPointer StylusPick::getDefaultResult(const QVariantMap& pickVariant) c } PickResultPointer StylusPick::getEntityIntersection(const StylusTip& pick) { - std::vector results; + auto entityTree = qApp->getEntities()->getTree(); + StylusPickResult nearestTarget(pick.toVariantMap()); for (const auto& target : getIncludeItems()) { if (target.isNull()) { continue; } - auto entity = qApp->getEntities()->getTree()->findEntityByEntityItemID(target); + auto entity = entityTree->findEntityByEntityItemID(target); if (!entity) { continue; } @@ -154,31 +155,45 @@ PickResultPointer StylusPick::getEntityIntersection(const StylusTip& pick) { const auto entityRotation = entity->getWorldOrientation(); const auto entityPosition = entity->getWorldPosition(); + const auto entityType = entity->getType(); + glm::vec3 normal; - glm::vec3 normal = entityRotation * Vectors::UNIT_Z; + // TODO: Use the xz projection method for Sphere and Quad. + if (entityType == EntityTypes::Gizmo) { + normal = entityRotation * Vectors::UNIT_Y; + } else { + normal = entityRotation * Vectors::UNIT_Z; + } float distance = glm::dot(pick.position - entityPosition, normal); - glm::vec3 intersection = pick.position - (normal * distance); + if (distance < nearestTarget.distance) { + const auto entityDimensions = entity->getScaledDimensions(); + const auto entityRegistrationPoint = entity->getRegistrationPoint(); + glm::vec3 intersection = pick.position - (normal * distance); + glm::vec2 pos2D; - glm::vec2 pos2D = RayPick::projectOntoEntityXYPlane(target, intersection, false); - if (pos2D == glm::clamp(pos2D, glm::vec2(0), glm::vec2(1))) { - IntersectionType type = IntersectionType::ENTITY; - if (getFilter().doesPickLocalEntities()) { - EntityPropertyFlags desiredProperties; - desiredProperties += PROP_ENTITY_HOST_TYPE; - if (DependencyManager::get()->getEntityProperties(target, desiredProperties).getEntityHostType() == entity::HostType::LOCAL) { - type = IntersectionType::LOCAL_ENTITY; - } + + auto entityType = entity->getType(); + + if (entityType == EntityTypes::Gizmo) { + pos2D = RayPick::projectOntoXZPlane(intersection, entityPosition, entityRotation, + entityDimensions, entityRegistrationPoint, false); + } else { + pos2D = RayPick::projectOntoXYPlane(intersection, entityPosition, entityRotation, + entityDimensions, entityRegistrationPoint, false); + } + + if (pos2D == glm::clamp(pos2D, glm::vec2(0), glm::vec2(1))) { + IntersectionType type = IntersectionType::ENTITY; + if (getFilter().doesPickLocalEntities()) { + if (entity->getEntityHostType() == entity::HostType::LOCAL) { + type = IntersectionType::LOCAL_ENTITY; + } + } + nearestTarget = StylusPickResult(type, target, distance, intersection, pick, normal); } - results.push_back(StylusPickResult(type, target, distance, intersection, pick, normal)); } } - StylusPickResult nearestTarget(pick.toVariantMap()); - for (const auto& result : results) { - if (result.distance < nearestTarget.distance) { - nearestTarget = result; - } - } return std::make_shared(nearestTarget); } diff --git a/interface/src/scripting/AccountServicesScriptingInterface.cpp b/interface/src/scripting/AccountServicesScriptingInterface.cpp index a3597886e9..5f8fb065ff 100644 --- a/interface/src/scripting/AccountServicesScriptingInterface.cpp +++ b/interface/src/scripting/AccountServicesScriptingInterface.cpp @@ -115,8 +115,8 @@ DownloadInfoResult::DownloadInfoResult() : /**jsdoc * Information on the assets currently being downloaded and pending download. * @typedef {object} AccountServices.DownloadInfoResult - * @property {number[]} downloading - The percentage complete for each asset currently being downloaded. - * @property {number} pending - The number of assets waiting to be download. + * @property {number[]} downloading - The download percentage remaining of each asset currently downloading. + * @property {number} pending - The number of assets pending download. */ QScriptValue DownloadInfoResultToScriptValue(QScriptEngine* engine, const DownloadInfoResult& result) { QScriptValue object = engine->newObject(); diff --git a/interface/src/scripting/AccountServicesScriptingInterface.h b/interface/src/scripting/AccountServicesScriptingInterface.h index c08181d7c9..b188b4e63b 100644 --- a/interface/src/scripting/AccountServicesScriptingInterface.h +++ b/interface/src/scripting/AccountServicesScriptingInterface.h @@ -38,19 +38,19 @@ class AccountServicesScriptingInterface : public QObject { Q_OBJECT /**jsdoc - * The AccountServices API provides functions related to user connectivity, visibility, and asset download - * progress. + * The AccountServices API provides functions that give information on user connectivity, visibility, and + * asset download progress. * * @hifi-interface * @hifi-client-entity * @hifi-avatar * * @namespace AccountServices - * @property {string} username - The user name if the user is logged in, otherwise "Unknown user". - * Read-only. + * @property {string} username - The user name of the user logged in. If there is no user logged in, it is + * "Unknown user". Read-only. * @property {boolean} loggedIn - true if the user is logged in, otherwise false. * Read-only. - * @property {string} findableBy - The user's visibility to other people:
+ * @property {string} findableBy - The user's visibility to other users:
* "none" - user appears offline.
* "friends" - user is visible only to friends.
* "connections" - user is visible to friends and connections.
@@ -74,23 +74,23 @@ public: public slots: /**jsdoc - * Get information on the progress of downloading assets in the domain. + * Gets information on the download progress of assets in the domain. * @function AccountServices.getDownloadInfo - * @returns {AccountServices.DownloadInfoResult} Information on the progress of assets download. + * @returns {AccountServices.DownloadInfoResult} Information on the download progress of assets. */ DownloadInfoResult getDownloadInfo(); /**jsdoc - * Cause a {@link AccountServices.downloadInfoChanged|downloadInfoChanged} signal to be triggered with information on the - * current progress of the download of assets in the domain. + * Triggers a {@link AccountServices.downloadInfoChanged|downloadInfoChanged} signal with information on the current + * download progress of the assets in the domain. * @function AccountServices.updateDownloadInfo */ void updateDownloadInfo(); /**jsdoc - * Check whether the user is logged in. + * Checks whether the user is logged in. * @function AccountServices.isLoggedIn - * @returns {boolean} true if the user is logged in, false otherwise. + * @returns {boolean} true if the user is logged in, false if not. * @example Report whether you are logged in. * var isLoggedIn = AccountServices.isLoggedIn(); * print("You are logged in: " + isLoggedIn); // true or false @@ -98,9 +98,9 @@ public slots: bool isLoggedIn(); /**jsdoc - * Prompts the user to log in (the login dialog is displayed) if they're not already logged in. + * The function returns the login status of the user and prompts the user to log in (with a login dialog) if they're not already logged in. * @function AccountServices.checkAndSignalForAccessToken - * @returns {boolean} true if the user is already logged in, false otherwise. + * @returns {boolean} true if the user is logged in, false if not. */ bool checkAndSignalForAccessToken(); @@ -140,7 +140,7 @@ signals: /**jsdoc * Triggered when the username logged in with changes, i.e., when the user logs in or out. * @function AccountServices.myUsernameChanged - * @param {string} username - The username logged in with if the user is logged in, otherwise "". + * @param {string} username - The user name of the user logged in. If there is no user logged in, it is "". * @returns {Signal} * @example Report when your username changes. * AccountServices.myUsernameChanged.connect(function (username) { @@ -150,9 +150,9 @@ signals: void myUsernameChanged(const QString& username); /**jsdoc - * Triggered when the progress of the download of assets for the domain changes. + * Triggered when the download progress of the assets in the domain changes. * @function AccountServices.downloadInfoChanged - * @param {AccountServices.DownloadInfoResult} downloadInfo - Information on the progress of assets download. + * @param {AccountServices.DownloadInfoResult} downloadInfo - Information on the download progress of assets. * @returns {Signal} */ void downloadInfoChanged(DownloadInfoResult info); @@ -186,7 +186,7 @@ signals: /**jsdoc * Triggered when the login status of the user changes. * @function AccountServices.loggedInChanged - * @param {boolean} loggedIn - true if the user is logged in, otherwise false. + * @param {boolean} loggedIn - true if the user is logged in, false if not. * @returns {Signal} * @example Report when your login status changes. * AccountServices.loggedInChanged.connect(function(loggedIn) { diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index fb64dbe098..2c4c29ff65 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -27,8 +27,9 @@ QString Audio::HMD { "VR" }; Setting::Handle enableNoiseReductionSetting { QStringList { Audio::AUDIO, "NoiseReduction" }, true }; float Audio::loudnessToLevel(float loudness) { - float level = 6.02059991f * fastLog2f(loudness); // level in dBFS - level = (level + 48.0f) * (1/39.0f); // map [-48, -9] dBFS to [0, 1] + float level = loudness * (1/32768.0f); // level in [0, 1] + level = 6.02059991f * fastLog2f(level); // convert to dBFS + level = (level + 48.0f) * (1/42.0f); // map [-48, -6] dBFS to [0, 1] return glm::clamp(level, 0.0f, 1.0f); } diff --git a/interface/src/scripting/WalletScriptingInterface.h b/interface/src/scripting/WalletScriptingInterface.h index 7482b8be00..3ef9c7953a 100644 --- a/interface/src/scripting/WalletScriptingInterface.h +++ b/interface/src/scripting/WalletScriptingInterface.h @@ -41,8 +41,8 @@ public: * * @property {WalletScriptingInterface.WalletStatus} walletStatus - The status of the user's wallet. Read-only. * @property {boolean} limitedCommerce - true if Interface is running in limited commerce mode. In limited commerce - * mode, certain Interface functionality is disabled, e.g., users can't buy non-free items from the Marketplace. The Oculus - * Store version of Interface runs in limited commerce mode. Read-only. + * mode, certain Interface functionalities are disabled, e.g., users can't buy items that are not free from the Marketplace. + * The Oculus Store version of Interface runs in limited commerce mode. Read-only. */ class WalletScriptingInterface : public QObject, public Dependency { Q_OBJECT @@ -55,16 +55,16 @@ public: WalletScriptingInterface(); /**jsdoc - * Check and update the user's wallet status. + * Checks and updates the user's wallet status. * @function WalletScriptingInterface.refreshWalletStatus */ Q_INVOKABLE void refreshWalletStatus(); /**jsdoc - * Get the current status of the user's wallet. + * Gets the current status of the user's wallet. * @function WalletScriptingInterface.getWalletStatus * @returns {WalletScriptingInterface.WalletStatus} - * @example Two ways to report your wallet status. + * @example Use two methods to report your wallet's status. * print("Wallet status: " + WalletScriptingInterface.walletStatus); // Same value as next line. * print("Wallet status: " + WalletScriptingInterface.getWalletStatus()); */ @@ -74,11 +74,11 @@ public: * Check that a certified avatar entity is owned by the avatar whose entity it is. The result of the check is provided via * the {@link WalletScriptingInterface.ownershipVerificationSuccess|ownershipVerificationSuccess} and * {@link WalletScriptingInterface.ownershipVerificationFailed|ownershipVerificationFailed} signals.
- * Warning: Neither of these signals fire if the entity is not an avatar entity or it's not a certified - * entity. + * Warning: Neither of these signals are triggered if the entity is not an avatar entity or is not + * certified. * @function WalletScriptingInterface.proveAvatarEntityOwnershipVerification - * @param {Uuid} entityID - The ID of the avatar entity to check. - * @example Check ownership of all nearby certified avatar entities. + * @param {Uuid} entityID - The avatar entity's ID. + * @example Check the ownership of all nearby certified avatar entities. * // Set up response handling. * function ownershipSuccess(entityID) { * print("Ownership test succeeded for: " + entityID); @@ -118,7 +118,7 @@ public: signals: /**jsdoc - * Triggered when the status of the user's wallet changes. + * Triggered when the user's wallet status changes. * @function WalletScriptingInterface.walletStatusChanged * @returns {Signal} * @example Report when your wallet status changes, e.g., when you log in and out. @@ -136,7 +136,7 @@ signals: void limitedCommerceChanged(); /**jsdoc - * Triggered when the user rezzes a certified entity but the user's wallet is not ready and so the certified location of the + * Triggered when the user rezzes a certified entity but the user's wallet is not ready. So the certified location of the * entity cannot be updated in the metaverse. * @function WalletScriptingInterface.walletNotSetup * @returns {Signal} diff --git a/interface/src/ui/Keyboard.cpp b/interface/src/ui/Keyboard.cpp index 8102df6dc6..9b75f78e67 100644 --- a/interface/src/ui/Keyboard.cpp +++ b/interface/src/ui/Keyboard.cpp @@ -65,7 +65,7 @@ static const glm::vec3 KEYBOARD_TABLET_OFFSET{0.30f, -0.38f, -0.04f}; static const glm::vec3 KEYBOARD_TABLET_DEGREES_OFFSET{-45.0f, 0.0f, 0.0f}; static const glm::vec3 KEYBOARD_TABLET_LANDSCAPE_OFFSET{-0.2f, -0.27f, -0.05f}; static const glm::vec3 KEYBOARD_TABLET_LANDSCAPE_DEGREES_OFFSET{-45.0f, 0.0f, -90.0f}; -static const glm::vec3 KEYBOARD_AVATAR_OFFSET{-0.6f, 0.3f, -0.7f}; +static const glm::vec3 KEYBOARD_AVATAR_OFFSET{-0.3f, 0.0f, -0.7f}; static const glm::vec3 KEYBOARD_AVATAR_DEGREES_OFFSET{0.0f, 180.0f, 0.0f}; static const QString SOUND_FILE = PathUtils::resourcesUrl() + "sounds/keyboardPress.mp3"; @@ -259,6 +259,12 @@ void Keyboard::setUse3DKeyboard(bool use) { void Keyboard::createKeyboard() { auto pointerManager = DependencyManager::get(); + if (_created) { + pointerManager->removePointer(_leftHandStylus); + pointerManager->removePointer(_rightHandStylus); + clearKeyboardKeys(); + } + QVariantMap modelProperties { { "url", MALLET_MODEL_URL } }; @@ -289,6 +295,8 @@ void Keyboard::createKeyboard() { loadKeyboardFile(keyboardSvg); _keySound = DependencyManager::get()->getSound(SOUND_FILE); + + _created = true; } bool Keyboard::isRaised() const { @@ -309,12 +317,22 @@ void Keyboard::setRaised(bool raised) { _layerIndex = 0; _capsEnabled = false; _typedCharacters.clear(); + addIncludeItemsToMallets(); }); updateTextDisplay(); } } +void Keyboard::addIncludeItemsToMallets() { + if (_layerIndex >= 0 && _layerIndex < (int)_keyboardLayers.size()) { + QVector includeItems = _keyboardLayers[_layerIndex].keys().toVector(); + auto pointerManager = DependencyManager::get(); + pointerManager->setIncludeItems(_leftHandStylus, includeItems); + pointerManager->setIncludeItems(_rightHandStylus, includeItems); + } +} + void Keyboard::updateTextDisplay() { auto myAvatar = DependencyManager::get()->getMyAvatar(); auto entityScriptingInterface = DependencyManager::get(); @@ -355,6 +373,12 @@ void Keyboard::raiseKeyboardAnchor(bool raise) const { void Keyboard::scaleKeyboard(float sensorToWorldScale) { auto entityScriptingInterface = DependencyManager::get(); + { + EntityItemProperties properties; + properties.setDimensions(_anchor.originalDimensions * sensorToWorldScale); + entityScriptingInterface->editEntity(_anchor.entityID, properties); + } + { EntityItemProperties properties; properties.setLocalPosition(_backPlate.localPosition * sensorToWorldScale); @@ -463,6 +487,8 @@ void Keyboard::switchToLayer(int layerIndex) { properties.setRotation(currentOrientation); entityScriptingInterface->editEntity(_anchor.entityID, properties); + addIncludeItemsToMallets(); + startLayerSwitchTimer(); } } @@ -718,8 +744,6 @@ void Keyboard::loadKeyboardFile(const QString& keyboardFile) { clearKeyboardKeys(); auto requestData = request->getData(); - QVector includeItems; - QJsonParseError parseError; QJsonDocument jsonDoc = QJsonDocument::fromJson(requestData, &parseError); @@ -840,7 +864,6 @@ void Keyboard::loadKeyboardFile(const QString& keyboardFile) { key.setKeyString(keyString); key.saveDimensionsAndLocalPosition(); - includeItems.append(key.getID()); _itemsToIgnore.insert(key.getID()); keyboardLayerKeys.insert(id, key); } @@ -886,9 +909,7 @@ void Keyboard::loadKeyboardFile(const QString& keyboardFile) { _itemsToIgnore.insert(_anchor.entityID); }); _layerIndex = 0; - auto pointerManager = DependencyManager::get(); - pointerManager->setIncludeItems(_leftHandStylus, includeItems); - pointerManager->setIncludeItems(_rightHandStylus, includeItems); + addIncludeItemsToMallets(); }); request->send(); diff --git a/interface/src/ui/Keyboard.h b/interface/src/ui/Keyboard.h index 958c862520..b3358e486d 100644 --- a/interface/src/ui/Keyboard.h +++ b/interface/src/ui/Keyboard.h @@ -157,6 +157,7 @@ private: bool shouldProcessEntityAndPointerEvent(const PointerEvent& event) const; bool shouldProcessPointerEvent(const PointerEvent& event) const; bool shouldProcessEntity() const; + void addIncludeItemsToMallets(); void startLayerSwitchTimer(); bool isLayerSwitchTimerFinished() const; @@ -178,7 +179,12 @@ private: mutable ReadWriteLockable _handLaserLock; mutable ReadWriteLockable _preferMalletsOverLasersSettingLock; mutable ReadWriteLockable _ignoreItemsLock; + +#ifdef Q_OS_ANDROID + Setting::Handle _use3DKeyboard { "use3DKeyboard", false }; +#else Setting::Handle _use3DKeyboard { "use3DKeyboard", true }; +#endif QString _typedCharacters; TextDisplay _textDisplay; @@ -187,6 +193,8 @@ private: QSet _itemsToIgnore; std::vector> _keyboardLayers; + + bool _created { false }; }; #endif diff --git a/interface/src/ui/overlays/ContextOverlayInterface.cpp b/interface/src/ui/overlays/ContextOverlayInterface.cpp index c382c3de43..e5cec70f64 100644 --- a/interface/src/ui/overlays/ContextOverlayInterface.cpp +++ b/interface/src/ui/overlays/ContextOverlayInterface.cpp @@ -50,11 +50,14 @@ ContextOverlayInterface::ContextOverlayInterface() { _entityPropertyFlags += PROP_OWNING_AVATAR_ID; auto entityScriptingInterface = DependencyManager::get().data(); - connect(entityScriptingInterface, &EntityScriptingInterface::clickDownOnEntity, this, &ContextOverlayInterface::clickDownOnEntity); - connect(entityScriptingInterface, &EntityScriptingInterface::holdingClickOnEntity, this, &ContextOverlayInterface::holdingClickOnEntity); + connect(entityScriptingInterface, &EntityScriptingInterface::mousePressOnEntity, this, &ContextOverlayInterface::clickDownOnEntity); connect(entityScriptingInterface, &EntityScriptingInterface::mouseReleaseOnEntity, this, &ContextOverlayInterface::mouseReleaseOnEntity); connect(entityScriptingInterface, &EntityScriptingInterface::hoverEnterEntity, this, &ContextOverlayInterface::contextOverlays_hoverEnterEntity); connect(entityScriptingInterface, &EntityScriptingInterface::hoverLeaveEntity, this, &ContextOverlayInterface::contextOverlays_hoverLeaveEntity); + + connect(&qApp->getOverlays(), &Overlays::hoverEnterOverlay, this, &ContextOverlayInterface::contextOverlays_hoverEnterOverlay); + connect(&qApp->getOverlays(), &Overlays::hoverLeaveOverlay, this, &ContextOverlayInterface::contextOverlays_hoverLeaveOverlay); + connect(_tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system"), &TabletProxy::tabletShownChanged, this, [&]() { if (_contextOverlayJustClicked && _hmdScriptingInterface->isMounted()) { QUuid tabletFrameID = _hmdScriptingInterface->getCurrentTabletFrameID(); @@ -70,9 +73,6 @@ ContextOverlayInterface::ContextOverlayInterface() { } }); connect(entityScriptingInterface, &EntityScriptingInterface::deletingEntity, this, &ContextOverlayInterface::deletingEntity); - connect(&qApp->getOverlays(), &Overlays::mousePressOnOverlay, this, &ContextOverlayInterface::contextOverlays_mousePressOnOverlay); - connect(&qApp->getOverlays(), &Overlays::hoverEnterOverlay, this, &ContextOverlayInterface::contextOverlays_hoverEnterOverlay); - connect(&qApp->getOverlays(), &Overlays::hoverLeaveOverlay, this, &ContextOverlayInterface::contextOverlays_hoverLeaveOverlay); { _selectionScriptingInterface->enableListHighlight("contextOverlayHighlightList", QVariantMap()); @@ -103,10 +103,14 @@ void ContextOverlayInterface::setEnabled(bool enabled) { } } -void ContextOverlayInterface::clickDownOnEntity(const EntityItemID& entityItemID, const PointerEvent& event) { - if (_enabled && event.getButton() == PointerEvent::SecondaryButton && contextOverlayFilterPassed(entityItemID)) { - _mouseDownEntity = entityItemID; +void ContextOverlayInterface::clickDownOnEntity(const EntityItemID& id, const PointerEvent& event) { + if (_enabled && event.getButton() == PointerEvent::SecondaryButton && contextOverlayFilterPassed(id)) { + _mouseDownEntity = id; _mouseDownEntityTimestamp = usecTimestampNow(); + } else if ((event.shouldFocus() || event.getButton() == PointerEvent::PrimaryButton) && id == _contextOverlayID) { + qCDebug(context_overlay) << "Clicked Context Overlay. Entity ID:" << _currentEntityWithContextOverlay << "ID:" << id; + emit contextOverlayClicked(_currentEntityWithContextOverlay); + _contextOverlayJustClicked = true; } else { if (!_currentEntityWithContextOverlay.isNull()) { disableEntityHighlight(_currentEntityWithContextOverlay); @@ -116,13 +120,10 @@ void ContextOverlayInterface::clickDownOnEntity(const EntityItemID& entityItemID } static const float CONTEXT_OVERLAY_CLICK_HOLD_TIME_MSEC = 400.0f; -void ContextOverlayInterface::holdingClickOnEntity(const EntityItemID& entityItemID, const PointerEvent& event) { +void ContextOverlayInterface::mouseReleaseOnEntity(const EntityItemID& entityItemID, const PointerEvent& event) { if (!_mouseDownEntity.isNull() && ((usecTimestampNow() - _mouseDownEntityTimestamp) > (CONTEXT_OVERLAY_CLICK_HOLD_TIME_MSEC * USECS_PER_MSEC))) { _mouseDownEntity = EntityItemID(); } -} - -void ContextOverlayInterface::mouseReleaseOnEntity(const EntityItemID& entityItemID, const PointerEvent& event) { if (_enabled && event.getButton() == PointerEvent::SecondaryButton && contextOverlayFilterPassed(entityItemID) && _mouseDownEntity == entityItemID) { createOrDestroyContextOverlay(entityItemID, event); } @@ -249,16 +250,8 @@ bool ContextOverlayInterface::destroyContextOverlay(const EntityItemID& entityIt return ContextOverlayInterface::destroyContextOverlay(entityItemID, PointerEvent()); } -void ContextOverlayInterface::contextOverlays_mousePressOnOverlay(const QUuid& id, const PointerEvent& event) { - if (id == _contextOverlayID && event.getButton() == PointerEvent::PrimaryButton) { - qCDebug(context_overlay) << "Clicked Context Overlay. Entity ID:" << _currentEntityWithContextOverlay << "ID:" << id; - emit contextOverlayClicked(_currentEntityWithContextOverlay); - _contextOverlayJustClicked = true; - } -} - void ContextOverlayInterface::contextOverlays_hoverEnterOverlay(const QUuid& id, const PointerEvent& event) { - if (_contextOverlayID != UNKNOWN_ENTITY_ID) { + if (_contextOverlayID == id) { qCDebug(context_overlay) << "Started hovering over Context Overlay. ID:" << id; EntityItemProperties properties; properties.setColor(CONTEXT_OVERLAY_COLOR); @@ -270,7 +263,7 @@ void ContextOverlayInterface::contextOverlays_hoverEnterOverlay(const QUuid& id, } void ContextOverlayInterface::contextOverlays_hoverLeaveOverlay(const QUuid& id, const PointerEvent& event) { - if (_contextOverlayID != UNKNOWN_ENTITY_ID) { + if (_contextOverlayID == id) { qCDebug(context_overlay) << "Stopped hovering over Context Overlay. ID:" << id; EntityItemProperties properties; properties.setColor(CONTEXT_OVERLAY_COLOR); diff --git a/interface/src/ui/overlays/ContextOverlayInterface.h b/interface/src/ui/overlays/ContextOverlayInterface.h index b87535acf2..b1b62aa0c4 100644 --- a/interface/src/ui/overlays/ContextOverlayInterface.h +++ b/interface/src/ui/overlays/ContextOverlayInterface.h @@ -59,13 +59,11 @@ signals: public slots: void clickDownOnEntity(const EntityItemID& entityItemID, const PointerEvent& event); - void holdingClickOnEntity(const EntityItemID& entityItemID, const PointerEvent& event); void mouseReleaseOnEntity(const EntityItemID& entityItemID, const PointerEvent& event); bool createOrDestroyContextOverlay(const EntityItemID& entityItemID, const PointerEvent& event); bool destroyContextOverlay(const EntityItemID& entityItemID, const PointerEvent& event); bool destroyContextOverlay(const EntityItemID& entityItemID); - void contextOverlays_mousePressOnOverlay(const QUuid& id, const PointerEvent& event); void contextOverlays_hoverEnterOverlay(const QUuid& id, const PointerEvent& event); void contextOverlays_hoverLeaveOverlay(const QUuid& id, const PointerEvent& event); void contextOverlays_hoverEnterEntity(const EntityItemID& entityID, const PointerEvent& event); diff --git a/interface/src/ui/overlays/Overlay.cpp b/interface/src/ui/overlays/Overlay.cpp index 714db97bc2..bf79a46dcb 100644 --- a/interface/src/ui/overlays/Overlay.cpp +++ b/interface/src/ui/overlays/Overlay.cpp @@ -16,8 +16,7 @@ #include "Application.h" Overlay::Overlay() : - _renderItemID(render::Item::INVALID_ITEM_ID), - _visible(true) + _renderItemID(render::Item::INVALID_ITEM_ID) { } @@ -34,20 +33,6 @@ void Overlay::setProperties(const QVariantMap& properties) { } } -QVariant Overlay::getProperty(const QString& property) { - if (property == "type") { - return QVariant(getType()); - } - if (property == "id") { - return getID(); - } - if (property == "visible") { - return _visible; - } - - return QVariant(); -} - bool Overlay::addToScene(Overlay::Pointer overlay, const render::ScenePointer& scene, render::Transaction& transaction) { _renderItemID = scene->allocateID(); transaction.resetItem(_renderItemID, std::make_shared(overlay)); @@ -65,7 +50,7 @@ render::ItemKey Overlay::getKey() { builder.withViewSpace(); builder.withLayer(render::hifi::LAYER_2D); - if (!getVisible()) { + if (!_visible) { builder.withInvisible(); } diff --git a/interface/src/ui/overlays/Overlay.h b/interface/src/ui/overlays/Overlay.h index ee6e281193..72373d2d20 100644 --- a/interface/src/ui/overlays/Overlay.h +++ b/interface/src/ui/overlays/Overlay.h @@ -31,7 +31,6 @@ public: virtual render::ItemKey getKey(); virtual AABox getBounds() const = 0; - virtual bool supportsGetProperty() const { return true; } virtual bool addToScene(Overlay::Pointer overlay, const render::ScenePointer& scene, render::Transaction& transaction); virtual void removeFromScene(Overlay::Pointer overlay, const render::ScenePointer& scene, render::Transaction& transaction); @@ -42,17 +41,15 @@ public: // getters virtual QString getType() const = 0; - bool isLoaded() { return true; } bool getVisible() const { return _visible; } // setters - virtual void setVisible(bool visible) { _visible = visible; } + void setVisible(bool visible) { _visible = visible; } unsigned int getStackOrder() const { return _stackOrder; } void setStackOrder(unsigned int stackOrder) { _stackOrder = stackOrder; } - Q_INVOKABLE virtual void setProperties(const QVariantMap& properties); + Q_INVOKABLE virtual void setProperties(const QVariantMap& properties) = 0; Q_INVOKABLE virtual Overlay* createClone() const = 0; - Q_INVOKABLE virtual QVariant getProperty(const QString& property); render::ItemID getRenderItemID() const { return _renderItemID; } void setRenderItemID(render::ItemID renderItemID) { _renderItemID = renderItemID; } @@ -60,7 +57,7 @@ public: protected: render::ItemID _renderItemID { render::Item::INVALID_ITEM_ID }; - bool _visible; + bool _visible { true }; unsigned int _stackOrder { 0 }; private: diff --git a/interface/src/ui/overlays/Overlay2D.cpp b/interface/src/ui/overlays/Overlay2D.cpp index 71b74e9452..91c7198e49 100644 --- a/interface/src/ui/overlays/Overlay2D.cpp +++ b/interface/src/ui/overlays/Overlay2D.cpp @@ -65,24 +65,4 @@ void Overlay2D::setProperties(const QVariantMap& properties) { } setBounds(newBounds); } -} - -QVariant Overlay2D::getProperty(const QString& property) { - if (property == "bounds") { - return qRectToVariant(_bounds); - } - if (property == "x") { - return _bounds.x(); - } - if (property == "y") { - return _bounds.y(); - } - if (property == "width") { - return _bounds.width(); - } - if (property == "height") { - return _bounds.height(); - } - - return Overlay::getProperty(property); -} +} \ No newline at end of file diff --git a/interface/src/ui/overlays/Overlay2D.h b/interface/src/ui/overlays/Overlay2D.h index 54ab52b469..cfcb114398 100644 --- a/interface/src/ui/overlays/Overlay2D.h +++ b/interface/src/ui/overlays/Overlay2D.h @@ -26,10 +26,6 @@ public: virtual uint32_t fetchMetaSubItems(render::ItemIDs& subItems) const override { subItems.push_back(getRenderItemID()); return 1; } // getters - int getX() const { return _bounds.x(); } - int getY() const { return _bounds.y(); } - int getWidth() const { return _bounds.width(); } - int getHeight() const { return _bounds.height(); } const QRect& getBoundingRect() const { return _bounds; } // setters @@ -40,7 +36,6 @@ public: void setBounds(const QRect& bounds) { _bounds = bounds; } void setProperties(const QVariantMap& properties) override; - QVariant getProperty(const QString& property) override; protected: QRect _bounds; // where on the screen to draw diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index e1708c14fe..1bcb040a77 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -43,14 +43,6 @@ std::unordered_map Overlays::_entityToOverlayTypes; std::unordered_map Overlays::_overlayToEntityTypes; Overlays::Overlays() { - auto pointerManager = DependencyManager::get(); - connect(pointerManager.data(), &PointerManager::hoverBeginOverlay, this, &Overlays::hoverEnterPointerEvent); - connect(pointerManager.data(), &PointerManager::hoverContinueOverlay, this, &Overlays::hoverOverPointerEvent); - connect(pointerManager.data(), &PointerManager::hoverEndOverlay, this, &Overlays::hoverLeavePointerEvent); - connect(pointerManager.data(), &PointerManager::triggerBeginOverlay, this, &Overlays::mousePressPointerEvent); - connect(pointerManager.data(), &PointerManager::triggerContinueOverlay, this, &Overlays::mouseMovePointerEvent); - connect(pointerManager.data(), &PointerManager::triggerEndOverlay, this, &Overlays::mouseReleasePointerEvent); - ADD_TYPE_MAP(Box, cube); ADD_TYPE_MAP(Sphere, sphere); _overlayToEntityTypes["rectangle3d"] = "Shape"; @@ -63,13 +55,6 @@ Overlays::Overlays() { ADD_TYPE_MAP(PolyLine, line3d); ADD_TYPE_MAP(Grid, grid); ADD_TYPE_MAP(Gizmo, circle3d); - - auto mouseRayPick = std::make_shared(Vectors::ZERO, Vectors::UP, - PickFilter(PickFilter::getBitMask(PickFilter::FlagBit::LOCAL_ENTITIES) | - PickFilter::getBitMask(PickFilter::FlagBit::VISIBLE)), 0.0f, true); - mouseRayPick->parentTransform = std::make_shared(); - mouseRayPick->setJointState(PickQuery::JOINT_STATE_MOUSE); - _mouseRayPickID = DependencyManager::get()->addPick(PickQuery::Ray, mouseRayPick); } void Overlays::cleanupAllOverlays() { @@ -87,13 +72,24 @@ void Overlays::cleanupAllOverlays() { } void Overlays::init() { - auto entityScriptingInterface = DependencyManager::get(); - connect(this, &Overlays::hoverEnterOverlay, entityScriptingInterface.data(), &EntityScriptingInterface::hoverEnterEntity); - connect(this, &Overlays::hoverOverOverlay, entityScriptingInterface.data(), &EntityScriptingInterface::hoverOverEntity); - connect(this, &Overlays::hoverLeaveOverlay, entityScriptingInterface.data(), &EntityScriptingInterface::hoverLeaveEntity); - connect(this, &Overlays::mousePressOnOverlay, entityScriptingInterface.data(), &EntityScriptingInterface::mousePressOnEntity); - connect(this, &Overlays::mouseMoveOnOverlay, entityScriptingInterface.data(), &EntityScriptingInterface::mouseMoveOnEntity); - connect(this, &Overlays::mouseReleaseOnOverlay, entityScriptingInterface.data(), &EntityScriptingInterface::mouseReleaseOnEntity); + auto entityScriptingInterface = DependencyManager::get().data(); + auto pointerManager = DependencyManager::get().data(); + connect(pointerManager, &PointerManager::hoverBeginOverlay, entityScriptingInterface , &EntityScriptingInterface::hoverEnterEntity); + connect(pointerManager, &PointerManager::hoverContinueOverlay, entityScriptingInterface, &EntityScriptingInterface::hoverOverEntity); + connect(pointerManager, &PointerManager::hoverEndOverlay, entityScriptingInterface, &EntityScriptingInterface::hoverLeaveEntity); + connect(pointerManager, &PointerManager::triggerBeginOverlay, entityScriptingInterface, &EntityScriptingInterface::mousePressOnEntity); + connect(pointerManager, &PointerManager::triggerContinueOverlay, entityScriptingInterface, &EntityScriptingInterface::mouseMoveOnEntity); + connect(pointerManager, &PointerManager::triggerEndOverlay, entityScriptingInterface, &EntityScriptingInterface::mouseReleaseOnEntity); + + connect(entityScriptingInterface, &EntityScriptingInterface::mousePressOnEntity, this, &Overlays::mousePressOnPointerEvent); + connect(entityScriptingInterface, &EntityScriptingInterface::mousePressOffEntity, this, &Overlays::mousePressOffPointerEvent); + connect(entityScriptingInterface, &EntityScriptingInterface::mouseDoublePressOnEntity, this, &Overlays::mouseDoublePressOnPointerEvent); + connect(entityScriptingInterface, &EntityScriptingInterface::mouseDoublePressOffEntity, this, &Overlays::mouseDoublePressOffPointerEvent); + connect(entityScriptingInterface, &EntityScriptingInterface::mouseReleaseOnEntity, this, &Overlays::mouseReleasePointerEvent); + connect(entityScriptingInterface, &EntityScriptingInterface::mouseMoveOnEntity, this, &Overlays::mouseMovePointerEvent); + connect(entityScriptingInterface, &EntityScriptingInterface::hoverEnterEntity , this, &Overlays::hoverEnterPointerEvent); + connect(entityScriptingInterface, &EntityScriptingInterface::hoverOverEntity, this, &Overlays::hoverOverPointerEvent); + connect(entityScriptingInterface, &EntityScriptingInterface::hoverLeaveEntity, this, &Overlays::hoverLeavePointerEvent); } void Overlays::update(float deltatime) { @@ -208,7 +204,8 @@ QString Overlays::overlayToEntityType(const QString& type) { #define RENAME_PROP(o, e) \ { \ auto iter = overlayProps.find(#o); \ - if (iter != overlayProps.end()) { \ + if (iter != overlayProps.end() && \ + !overlayProps.contains(#e)) { \ overlayProps[#e] = iter.value(); \ } \ } @@ -301,14 +298,14 @@ QString Overlays::overlayToEntityType(const QString& type) { } \ } -static QHash savedRotations = QHash(); +static QHash> savedRotations = QHash>(); EntityItemProperties Overlays::convertOverlayToEntityProperties(QVariantMap& overlayProps, const QString& type, bool add, const QUuid& id) { - glm::quat rotation; + std::pair rotation; return convertOverlayToEntityProperties(overlayProps, rotation, type, add, id); } -EntityItemProperties Overlays::convertOverlayToEntityProperties(QVariantMap& overlayProps, glm::quat& rotationToSave, const QString& type, bool add, const QUuid& id) { +EntityItemProperties Overlays::convertOverlayToEntityProperties(QVariantMap& overlayProps, std::pair& rotationToSave, const QString& type, bool add, const QUuid& id) { overlayProps["type"] = type; SET_OVERLAY_PROP_DEFAULT(alpha, 0.7); @@ -317,7 +314,11 @@ EntityItemProperties Overlays::convertOverlayToEntityProperties(QVariantMap& ove RENAME_PROP(start, position); } RENAME_PROP(point, position); - RENAME_PROP(scale, dimensions); + if (type != "Model") { + RENAME_PROP(scale, dimensions); + } else { + RENAME_PROP(scale, modelScale); + } RENAME_PROP(size, dimensions); RENAME_PROP(orientation, rotation); RENAME_PROP(localOrientation, localRotation); @@ -358,6 +359,17 @@ EntityItemProperties Overlays::convertOverlayToEntityProperties(QVariantMap& ove return "none"; }); + RENAME_PROP_CONVERT(textures, textures, [](const QVariant& v) { + auto map = v.toMap(); + if (!map.isEmpty()) { + auto json = QJsonDocument::fromVariant(map); + if (!json.isNull()) { + return QVariant(QString(json.toJson())); + } + } + return v; + }); + if (type == "Shape" || type == "Box" || type == "Sphere" || type == "Gizmo") { RENAME_PROP(solid, isSolid); RENAME_PROP(isFilled, isSolid); @@ -375,6 +387,8 @@ EntityItemProperties Overlays::convertOverlayToEntityProperties(QVariantMap& ove RENAME_PROP(animationSettings, animation); } else if (type == "Image") { RENAME_PROP(url, imageURL); + } else if (type == "Text") { + RENAME_PROP(color, textColor); } else if (type == "Web") { RENAME_PROP(url, sourceUrl); RENAME_PROP_CONVERT(inputMode, inputMode, [](const QVariant& v) { return v.toString() == "Mouse" ? "mouse" : "touch"; }); @@ -402,32 +416,13 @@ EntityItemProperties Overlays::convertOverlayToEntityProperties(QVariantMap& ove overlayProps["dimensions"] = vec3toVariant(ratio * dimensions); } - if (add || overlayProps.contains("rotation")) { - glm::quat rotation; - { - auto iter = overlayProps.find("rotation"); - if (iter != overlayProps.end()) { - rotation = quatFromVariant(iter.value()); - } else if (!add) { - EntityPropertyFlags desiredProperties; - desiredProperties += PROP_ROTATION; - rotation = DependencyManager::get()->getEntityProperties(id, desiredProperties).getRotation(); - } - } + if (add && !overlayProps.contains("rotation") && !overlayProps.contains("localRotation")) { + overlayProps["rotation"] = quatToVariant(glm::angleAxis(-(float)M_PI_2, Vectors::RIGHT)); + } else if (overlayProps.contains("rotation")) { + glm::quat rotation = quatFromVariant(overlayProps["rotation"]); overlayProps["rotation"] = quatToVariant(glm::angleAxis(-(float)M_PI_2, rotation * Vectors::RIGHT) * rotation); - } - if (add || overlayProps.contains("localRotation")) { - glm::quat rotation; - { - auto iter = overlayProps.find("localRotation"); - if (iter != overlayProps.end()) { - rotation = quatFromVariant(iter.value()); - } else if (!add) { - EntityPropertyFlags desiredProperties; - desiredProperties += PROP_LOCAL_ROTATION; - rotation = DependencyManager::get()->getEntityProperties(id, desiredProperties).getLocalRotation(); - } - } + } else if (overlayProps.contains("localRotation")) { + glm::quat rotation = quatFromVariant(overlayProps["localRotation"]); overlayProps["localRotation"] = quatToVariant(glm::angleAxis(-(float)M_PI_2, rotation * Vectors::RIGHT) * rotation); } @@ -499,15 +494,34 @@ EntityItemProperties Overlays::convertOverlayToEntityProperties(QVariantMap& ove RENAME_PROP_CONVERT(p1, p1, [](const QVariant& v) { return vec3toVariant(glm::vec3(0.0f)); }); RENAME_PROP_CONVERT(p2, p2, [=](const QVariant& v) { glm::vec3 position; + bool hasPosition = false; + glm::quat rotation; + bool hasRotation = false; + auto iter2 = overlayProps.find("position"); if (iter2 != overlayProps.end()) { position = vec3FromVariant(iter2.value()); - } else if (!add) { - EntityPropertyFlags desiredProperties; - desiredProperties += PROP_POSITION; - position = DependencyManager::get()->getEntityProperties(id, desiredProperties).getPosition(); + hasPosition = true; } - return vec3toVariant(vec3FromVariant(v) - position); + iter2 = overlayProps.find("rotation"); + if (iter2 != overlayProps.end()) { + rotation = quatFromVariant(iter2.value()); + hasRotation = true; + } + + if (!add && !(hasPosition && hasRotation)) { + auto entity = DependencyManager::get()->getEntity(id); + if (entity) { + if (!hasPosition) { + position = entity->getWorldPosition(); + } + if (!hasRotation) { + rotation = entity->getWorldOrientation(); + } + } + } + + return vec3toVariant(glm::inverse(rotation) * (vec3FromVariant(v) - position)); }); RENAME_PROP(localStart, p1); @@ -554,27 +568,31 @@ EntityItemProperties Overlays::convertOverlayToEntityProperties(QVariantMap& ove if (type == "Text" || type == "Image" || type == "Grid" || type == "Web") { glm::quat originalRotation = ENTITY_ITEM_DEFAULT_ROTATION; + bool local = false; { auto iter = overlayProps.find("rotation"); if (iter != overlayProps.end()) { originalRotation = quatFromVariant(iter.value()); + local = false; } else { iter = overlayProps.find("localRotation"); if (iter != overlayProps.end()) { originalRotation = quatFromVariant(iter.value()); + local = true; } else if (!add) { auto iter2 = savedRotations.find(id); if (iter2 != savedRotations.end()) { - originalRotation = iter2.value(); + originalRotation = iter2.value().first; + local = iter2.value().second; } } } } if (!add) { - savedRotations[id] = originalRotation; + savedRotations[id] = { originalRotation, local }; } else { - rotationToSave = originalRotation; + rotationToSave = { originalRotation, local }; } glm::vec3 dimensions = ENTITY_ITEM_DEFAULT_DIMENSIONS; @@ -605,7 +623,11 @@ EntityItemProperties Overlays::convertOverlayToEntityProperties(QVariantMap& ove rotation = glm::angleAxis((float)M_PI, rotation * Vectors::UP) * rotation; } - overlayProps["localRotation"] = quatToVariant(rotation); + if (local) { + overlayProps["localRotation"] = quatToVariant(rotation); + } else { + overlayProps["rotation"] = quatToVariant(rotation); + } overlayProps["dimensions"] = vec3toVariant(glm::abs(dimensions)); } } @@ -629,7 +651,11 @@ QVariantMap Overlays::convertEntityToOverlayProperties(const EntityItemPropertie RENAME_PROP(position, start); } RENAME_PROP(position, point); - RENAME_PROP(dimensions, scale); + if (type != "Model") { + RENAME_PROP(dimensions, scale); + } else { + RENAME_PROP(modelScale, scale); + } RENAME_PROP(dimensions, size); RENAME_PROP(ignorePickIntersection, ignoreRayIntersection); @@ -675,6 +701,8 @@ QVariantMap Overlays::convertEntityToOverlayProperties(const EntityItemPropertie RENAME_PROP(animation, animationSettings); } else if (type == "Image") { RENAME_PROP(imageURL, url); + } else if (type == "Text") { + RENAME_PROP(textColor, color); } else if (type == "Web") { RENAME_PROP(sourceUrl, url); RENAME_PROP_CONVERT(inputMode, inputMode, [](const QVariant& v) { return v.toString() == "mouse" ? "Mouse" : "Touch"; }); @@ -747,29 +775,29 @@ QUuid Overlays::addOverlay(const QString& type, const QVariant& properties) { return UNKNOWN_ENTITY_ID; } - if (QThread::currentThread() != thread()) { - QUuid result; - PROFILE_RANGE(script, __FUNCTION__); - BLOCKING_INVOKE_METHOD(this, "addOverlay", Q_RETURN_ARG(QUuid, result), Q_ARG(const QString&, type), Q_ARG(const QVariant&, properties)); - return result; - } - - Overlay::Pointer overlay; - if (type == ImageOverlay::TYPE) { + if (type == ImageOverlay::TYPE || type == TextOverlay::TYPE || type == RectangleOverlay::TYPE) { #if !defined(DISABLE_QML) - overlay = Overlay::Pointer(new ImageOverlay(), [](Overlay* ptr) { ptr->deleteLater(); }); -#endif - } else if (type == TextOverlay::TYPE) { -#if !defined(DISABLE_QML) - overlay = Overlay::Pointer(new TextOverlay(), [](Overlay* ptr) { ptr->deleteLater(); }); -#endif - } else if (type == RectangleOverlay::TYPE) { - overlay = Overlay::Pointer(new RectangleOverlay(), [](Overlay* ptr) { ptr->deleteLater(); }); - } + if (QThread::currentThread() != thread()) { + QUuid result; + PROFILE_RANGE(script, __FUNCTION__); + BLOCKING_INVOKE_METHOD(this, "addOverlay", Q_RETURN_ARG(QUuid, result), Q_ARG(const QString&, type), Q_ARG(const QVariant&, properties)); + return result; + } - if (overlay) { - overlay->setProperties(properties.toMap()); - return add2DOverlay(overlay); + Overlay::Pointer overlay; + if (type == ImageOverlay::TYPE) { + overlay = Overlay::Pointer(new ImageOverlay(), [](Overlay* ptr) { ptr->deleteLater(); }); + } else if (type == TextOverlay::TYPE) { + overlay = Overlay::Pointer(new TextOverlay(), [](Overlay* ptr) { ptr->deleteLater(); }); + } else if (type == RectangleOverlay::TYPE) { + overlay = Overlay::Pointer(new RectangleOverlay(), [](Overlay* ptr) { ptr->deleteLater(); }); + } + if (overlay) { + overlay->setProperties(properties.toMap()); + return add2DOverlay(overlay); + } +#endif + return QUuid(); } QString entityType = overlayToEntityType(type); @@ -781,7 +809,7 @@ QUuid Overlays::addOverlay(const QString& type, const QVariant& properties) { if (type == "rectangle3d") { propertyMap["shape"] = "Quad"; } - glm::quat rotationToSave; + std::pair rotationToSave; QUuid id = DependencyManager::get()->addEntityInternal(convertOverlayToEntityProperties(propertyMap, rotationToSave, entityType, true), entity::HostType::LOCAL); if (entityType == "Text" || entityType == "Image" || entityType == "Grid" || entityType == "Web") { savedRotations[id] = rotationToSave; @@ -810,15 +838,14 @@ QUuid Overlays::cloneOverlay(const QUuid& id) { return UNKNOWN_ENTITY_ID; } - if (QThread::currentThread() != thread()) { - QUuid result; - PROFILE_RANGE(script, __FUNCTION__); - BLOCKING_INVOKE_METHOD(this, "cloneOverlay", Q_RETURN_ARG(QUuid, result), Q_ARG(const QUuid&, id)); - return result; - } - Overlay::Pointer overlay = get2DOverlay(id); if (overlay) { + if (QThread::currentThread() != thread()) { + QUuid result; + PROFILE_RANGE(script, __FUNCTION__); + BLOCKING_INVOKE_METHOD(this, "cloneOverlay", Q_RETURN_ARG(QUuid, result), Q_ARG(const QUuid&, id)); + return result; + } return add2DOverlay(Overlay::Pointer(overlay->createClone(), [](Overlay* ptr) { ptr->deleteLater(); })); } @@ -894,6 +921,11 @@ void Overlays::deleteOverlay(const QUuid& id) { Overlay::Pointer overlay = take2DOverlay(id); if (overlay) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "deleteOverlay", Q_ARG(const QUuid&, id)); + return; + } + _overlaysToDelete.push_back(overlay); emit overlayDeleted(id); return; @@ -908,15 +940,14 @@ QString Overlays::getOverlayType(const QUuid& id) { return ""; } - if (QThread::currentThread() != thread()) { - QString result; - PROFILE_RANGE(script, __FUNCTION__); - BLOCKING_INVOKE_METHOD(this, "getOverlayType", Q_RETURN_ARG(QString, result), Q_ARG(const QUuid&, id)); - return result; - } - Overlay::Pointer overlay = get2DOverlay(id); if (overlay) { + if (QThread::currentThread() != thread()) { + QString result; + PROFILE_RANGE(script, __FUNCTION__); + BLOCKING_INVOKE_METHOD(this, "getOverlayType", Q_RETURN_ARG(QString, result), Q_ARG(const QUuid&, id)); + return result; + } return overlay->getType(); } @@ -924,15 +955,14 @@ QString Overlays::getOverlayType(const QUuid& id) { } QObject* Overlays::getOverlayObject(const QUuid& id) { - if (QThread::currentThread() != thread()) { - QObject* result; - PROFILE_RANGE(script, __FUNCTION__); - BLOCKING_INVOKE_METHOD(this, "getOverlayObject", Q_RETURN_ARG(QObject*, result), Q_ARG(const QUuid&, id)); - return result; - } - Overlay::Pointer overlay = get2DOverlay(id); if (overlay) { + if (QThread::currentThread() != thread()) { + QObject* result; + PROFILE_RANGE(script, __FUNCTION__); + BLOCKING_INVOKE_METHOD(this, "getOverlayObject", Q_RETURN_ARG(QObject*, result), Q_ARG(const QUuid&, id)); + return result; + } return qobject_cast(&(*overlay)); } @@ -944,6 +974,12 @@ QUuid Overlays::getOverlayAtPoint(const glm::vec2& point) { return UNKNOWN_ENTITY_ID; } + if (QThread::currentThread() != thread()) { + QUuid result; + BLOCKING_INVOKE_METHOD(this, "getOverlayAtPoint", Q_RETURN_ARG(QUuid, result), Q_ARG(const glm::vec2&, point)); + return result; + } + QMutexLocker locker(&_mutex); QMapIterator i(_overlays); unsigned int bestStackOrder = 0; @@ -951,8 +987,7 @@ QUuid Overlays::getOverlayAtPoint(const glm::vec2& point) { while (i.hasNext()) { i.next(); auto thisOverlay = std::dynamic_pointer_cast(i.value()); - if (thisOverlay && thisOverlay->getVisible() && thisOverlay->isLoaded() && - thisOverlay->getBoundingRect().contains(point.x, point.y, false)) { + if (thisOverlay && thisOverlay->getVisible() && thisOverlay->getBoundingRect().contains(point.x, point.y, false)) { if (thisOverlay->getStackOrder() > bestStackOrder) { bestID = i.key(); bestStackOrder = thisOverlay->getStackOrder(); @@ -966,9 +1001,7 @@ QUuid Overlays::getOverlayAtPoint(const glm::vec2& point) { QVariant Overlays::getProperty(const QUuid& id, const QString& property) { Overlay::Pointer overlay = get2DOverlay(id); if (overlay) { - if (overlay->supportsGetProperty()) { - return overlay->getProperty(property); - } + // We don't support getting properties from QML Overlays right now return QVariant(); } @@ -984,12 +1017,8 @@ QVariantMap Overlays::getProperties(const QUuid& id, const QStringList& properti Overlay::Pointer overlay = get2DOverlay(id); QVariantMap result; if (overlay) { - if (overlay->supportsGetProperty()) { - for (const auto& property : properties) { - result.insert(property, overlay->getProperty(property)); - } - } - return result; + // We don't support getting properties from QML Overlays right now + return QVariantMap(); } QVariantMap overlayProperties = convertEntityToOverlayProperties(DependencyManager::get()->getEntityProperties(id)); @@ -1116,38 +1145,30 @@ void RayToOverlayIntersectionResultFromScriptValue(const QScriptValue& object, R } bool Overlays::isLoaded(const QUuid& id) { - if (QThread::currentThread() != thread()) { - bool result; - PROFILE_RANGE(script, __FUNCTION__); - BLOCKING_INVOKE_METHOD(this, "isLoaded", Q_RETURN_ARG(bool, result), Q_ARG(const QUuid&, id)); - return result; - } - Overlay::Pointer overlay = get2DOverlay(id); if (overlay) { - return overlay->isLoaded(); + return true; } return DependencyManager::get()->isLoaded(id); } QSizeF Overlays::textSize(const QUuid& id, const QString& text) { - if (QThread::currentThread() != thread()) { - QSizeF result; - PROFILE_RANGE(script, __FUNCTION__); - BLOCKING_INVOKE_METHOD(this, "textSize", Q_RETURN_ARG(QSizeF, result), Q_ARG(const QUuid&, id), Q_ARG(QString, text)); - return result; - } - Overlay::Pointer overlay = get2DOverlay(id); if (overlay) { + if (QThread::currentThread() != thread()) { + QSizeF result; + PROFILE_RANGE(script, __FUNCTION__); + BLOCKING_INVOKE_METHOD(this, "textSize", Q_RETURN_ARG(QSizeF, result), Q_ARG(const QUuid&, id), Q_ARG(QString, text)); + return result; + } if (auto textOverlay = std::dynamic_pointer_cast(overlay)) { return textOverlay->textSize(text); } return QSizeF(0.0f, 0.0f); - } else { - return DependencyManager::get()->textSize(id, text); } + + return DependencyManager::get()->textSize(id, text); } bool Overlays::isAddedOverlay(const QUuid& id) { @@ -1160,7 +1181,7 @@ bool Overlays::isAddedOverlay(const QUuid& id) { } void Overlays::sendMousePressOnOverlay(const QUuid& id, const PointerEvent& event) { - mousePressPointerEvent(id, event); + mousePressOnPointerEvent(id, event); } void Overlays::sendMouseReleaseOnOverlay(const QUuid& id, const PointerEvent& event) { @@ -1207,57 +1228,66 @@ float Overlays::height() { return offscreenUi->getWindow()->size().height(); } -static uint32_t toPointerButtons(const QMouseEvent& event) { - uint32_t buttons = 0; - buttons |= event.buttons().testFlag(Qt::LeftButton) ? PointerEvent::PrimaryButton : 0; - buttons |= event.buttons().testFlag(Qt::RightButton) ? PointerEvent::SecondaryButton : 0; - buttons |= event.buttons().testFlag(Qt::MiddleButton) ? PointerEvent::TertiaryButton : 0; - return buttons; -} - -static PointerEvent::Button toPointerButton(const QMouseEvent& event) { - switch (event.button()) { - case Qt::LeftButton: - return PointerEvent::PrimaryButton; - case Qt::RightButton: - return PointerEvent::SecondaryButton; - case Qt::MiddleButton: - return PointerEvent::TertiaryButton; - default: - return PointerEvent::NoButtons; - } -} - -RayToOverlayIntersectionResult getPrevPickResult(unsigned int mouseRayPickID) { - RayToOverlayIntersectionResult overlayResult; - overlayResult.intersects = false; - auto pickResult = DependencyManager::get()->getPrevPickResultTyped(mouseRayPickID); - if (pickResult) { - overlayResult.intersects = pickResult->type != IntersectionType::NONE; - if (overlayResult.intersects) { - overlayResult.intersection = pickResult->intersection; - overlayResult.distance = pickResult->distance; - overlayResult.surfaceNormal = pickResult->surfaceNormal; - overlayResult.overlayID = pickResult->objectID; - overlayResult.extraInfo = pickResult->extraInfo; +void Overlays::mousePressOnPointerEvent(const QUuid& id, const PointerEvent& event) { + auto keyboard = DependencyManager::get(); + // Do not send keyboard key event to scripts to prevent malignant scripts from gathering what you typed + if (!keyboard->getKeyIDs().contains(id)) { + auto entity = DependencyManager::get()->getEntity(id); + if (entity && entity->isLocalEntity()) { + emit mousePressOnOverlay(id, event); } } - return overlayResult; } -PointerEvent Overlays::calculateOverlayPointerEvent(const QUuid& id, const PickRay& ray, - const RayToOverlayIntersectionResult& rayPickResult, QMouseEvent* event, - PointerEvent::EventType eventType) { - glm::vec2 pos2D = RayPick::projectOntoEntityXYPlane(id, rayPickResult.intersection); - return PointerEvent(eventType, PointerManager::MOUSE_POINTER_ID, pos2D, rayPickResult.intersection, rayPickResult.surfaceNormal, - ray.direction, toPointerButton(*event), toPointerButtons(*event), event->modifiers()); +void Overlays::mousePressOffPointerEvent() { + emit mousePressOffOverlay(); +} + +void Overlays::mouseDoublePressOnPointerEvent(const QUuid& id, const PointerEvent& event) { + auto keyboard = DependencyManager::get(); + // Do not send keyboard key event to scripts to prevent malignant scripts from gathering what you typed + if (!keyboard->getKeyIDs().contains(id)) { + auto entity = DependencyManager::get()->getEntity(id); + if (entity && entity->isLocalEntity()) { + emit mouseDoublePressOnOverlay(id, event); + } + } +} + +void Overlays::mouseDoublePressOffPointerEvent() { + emit mouseDoublePressOffOverlay(); +} + +void Overlays::mouseReleasePointerEvent(const QUuid& id, const PointerEvent& event) { + auto keyboard = DependencyManager::get(); + // Do not send keyboard key event to scripts to prevent malignant scripts from gathering what you typed + if (!keyboard->getKeyIDs().contains(id)) { + auto entity = DependencyManager::get()->getEntity(id); + if (entity && entity->isLocalEntity()) { + emit mouseReleaseOnOverlay(id, event); + } + } +} + +void Overlays::mouseMovePointerEvent(const QUuid& id, const PointerEvent& event) { + auto keyboard = DependencyManager::get(); + // Do not send keyboard key event to scripts to prevent malignant scripts from gathering what you typed + if (!keyboard->getKeyIDs().contains(id)) { + auto entity = DependencyManager::get()->getEntity(id); + if (entity && entity->isLocalEntity()) { + emit mouseMoveOnOverlay(id, event); + } + } } void Overlays::hoverEnterPointerEvent(const QUuid& id, const PointerEvent& event) { auto keyboard = DependencyManager::get(); // Do not send keyboard key event to scripts to prevent malignant scripts from gathering what you typed if (!keyboard->getKeyIDs().contains(id)) { - emit hoverEnterOverlay(id, event); + auto entity = DependencyManager::get()->getEntity(id); + if (entity && entity->isLocalEntity()) { + emit hoverEnterOverlay(id, event); + } } } @@ -1265,7 +1295,10 @@ void Overlays::hoverOverPointerEvent(const QUuid& id, const PointerEvent& event) auto keyboard = DependencyManager::get(); // Do not send keyboard key event to scripts to prevent malignant scripts from gathering what you typed if (!keyboard->getKeyIDs().contains(id)) { - emit hoverOverOverlay(id, event); + auto entity = DependencyManager::get()->getEntity(id); + if (entity && entity->isLocalEntity()) { + emit hoverOverOverlay(id, event); + } } } @@ -1273,113 +1306,10 @@ void Overlays::hoverLeavePointerEvent(const QUuid& id, const PointerEvent& event auto keyboard = DependencyManager::get(); // Do not send keyboard key event to scripts to prevent malignant scripts from gathering what you typed if (!keyboard->getKeyIDs().contains(id)) { - emit hoverLeaveOverlay(id, event); - } -} - -std::pair Overlays::mousePressEvent(QMouseEvent* event) { - PerformanceTimer perfTimer("Overlays::mousePressEvent"); - - PickRay ray = qApp->computePickRay(event->x(), event->y()); - RayToOverlayIntersectionResult rayPickResult = getPrevPickResult(_mouseRayPickID); - if (rayPickResult.intersects) { - _currentClickingOnOverlayID = rayPickResult.overlayID; - - PointerEvent pointerEvent = calculateOverlayPointerEvent(_currentClickingOnOverlayID, ray, rayPickResult, event, PointerEvent::Press); - mousePressPointerEvent(_currentClickingOnOverlayID, pointerEvent); - return { rayPickResult.distance, rayPickResult.overlayID }; - } - emit mousePressOffOverlay(); - return { FLT_MAX, UNKNOWN_ENTITY_ID }; -} - -void Overlays::mousePressPointerEvent(const QUuid& id, const PointerEvent& event) { - auto keyboard = DependencyManager::get(); - // Do not send keyboard key event to scripts to prevent malignant scripts from gathering what you typed - if (!keyboard->getKeyIDs().contains(id)) { - emit mousePressOnOverlay(id, event); - } -} - -bool Overlays::mouseDoublePressEvent(QMouseEvent* event) { - PerformanceTimer perfTimer("Overlays::mouseDoublePressEvent"); - - PickRay ray = qApp->computePickRay(event->x(), event->y()); - RayToOverlayIntersectionResult rayPickResult = getPrevPickResult(_mouseRayPickID); - if (rayPickResult.intersects) { - _currentClickingOnOverlayID = rayPickResult.overlayID; - - auto pointerEvent = calculateOverlayPointerEvent(_currentClickingOnOverlayID, ray, rayPickResult, event, PointerEvent::Press); - emit mouseDoublePressOnOverlay(_currentClickingOnOverlayID, pointerEvent); - return true; - } - emit mouseDoublePressOffOverlay(); - return false; -} - -bool Overlays::mouseReleaseEvent(QMouseEvent* event) { - PerformanceTimer perfTimer("Overlays::mouseReleaseEvent"); - - PickRay ray = qApp->computePickRay(event->x(), event->y()); - RayToOverlayIntersectionResult rayPickResult = getPrevPickResult(_mouseRayPickID); - if (rayPickResult.intersects) { - auto pointerEvent = calculateOverlayPointerEvent(rayPickResult.overlayID, ray, rayPickResult, event, PointerEvent::Release); - mouseReleasePointerEvent(rayPickResult.overlayID, pointerEvent); - } - - _currentClickingOnOverlayID = UNKNOWN_ENTITY_ID; - return false; -} - -void Overlays::mouseReleasePointerEvent(const QUuid& id, const PointerEvent& event) { - auto keyboard = DependencyManager::get(); - // Do not send keyboard key event to scripts to prevent malignant scripts from gathering what you typed - if (!keyboard->getKeyIDs().contains(id)) { - emit mouseReleaseOnOverlay(id, event); - } -} - -bool Overlays::mouseMoveEvent(QMouseEvent* event) { - PerformanceTimer perfTimer("Overlays::mouseMoveEvent"); - - PickRay ray = qApp->computePickRay(event->x(), event->y()); - RayToOverlayIntersectionResult rayPickResult = getPrevPickResult(_mouseRayPickID); - if (rayPickResult.intersects) { - auto pointerEvent = calculateOverlayPointerEvent(rayPickResult.overlayID, ray, rayPickResult, event, PointerEvent::Move); - mouseMovePointerEvent(rayPickResult.overlayID, pointerEvent); - - // If previously hovering over a different overlay then leave hover on that overlay. - if (_currentHoverOverOverlayID != UNKNOWN_ENTITY_ID && rayPickResult.overlayID != _currentHoverOverOverlayID) { - auto pointerEvent = calculateOverlayPointerEvent(_currentHoverOverOverlayID, ray, rayPickResult, event, PointerEvent::Move); - hoverLeavePointerEvent(_currentHoverOverOverlayID, pointerEvent); + auto entity = DependencyManager::get()->getEntity(id); + if (entity && entity->isLocalEntity()) { + emit hoverLeaveOverlay(id, event); } - - // If hovering over a new overlay then enter hover on that overlay. - if (rayPickResult.overlayID != _currentHoverOverOverlayID) { - hoverEnterPointerEvent(rayPickResult.overlayID, pointerEvent); - } - - // Hover over current overlay. - hoverOverPointerEvent(rayPickResult.overlayID, pointerEvent); - - _currentHoverOverOverlayID = rayPickResult.overlayID; - } else { - // If previously hovering an overlay then leave hover. - if (_currentHoverOverOverlayID != UNKNOWN_ENTITY_ID) { - auto pointerEvent = calculateOverlayPointerEvent(_currentHoverOverOverlayID, ray, rayPickResult, event, PointerEvent::Move); - hoverLeavePointerEvent(_currentHoverOverOverlayID, pointerEvent); - - _currentHoverOverOverlayID = UNKNOWN_ENTITY_ID; - } - } - return false; -} - -void Overlays::mouseMovePointerEvent(const QUuid& id, const PointerEvent& event) { - auto keyboard = DependencyManager::get(); - // Do not send keyboard key event to scripts to prevent malignant scripts from gathering what you typed - if (!keyboard->getKeyIDs().contains(id)) { - emit mouseMoveOnOverlay(id, event); } } @@ -1709,7 +1639,8 @@ QVector Overlays::findOverlays(const glm::vec3& center, float radius) { * * @property {Vec3} position - The position of the overlay center. Synonyms: p1, point, and * start. - * @property {Vec3} dimensions - The dimensions of the overlay. Synonyms: scale, size. + * @property {Vec3} dimensions - The dimensions of the overlay. Synonyms: size. + * @property {Vec3} scale - The scale factor applied to the model's dimensions. * @property {Quat} rotation - The orientation of the overlay. Synonym: orientation. * @property {Vec3} localPosition - The local position of the overlay relative to its parent if the overlay has a * parentID set, otherwise the same value as position. diff --git a/interface/src/ui/overlays/Overlays.h b/interface/src/ui/overlays/Overlays.h index 7612779099..0b2994b872 100644 --- a/interface/src/ui/overlays/Overlays.h +++ b/interface/src/ui/overlays/Overlays.h @@ -112,11 +112,6 @@ public: const QVector& discard, bool visibleOnly = false, bool collidableOnly = false); - std::pair mousePressEvent(QMouseEvent* event); - bool mouseDoublePressEvent(QMouseEvent* event); - bool mouseReleaseEvent(QMouseEvent* event); - bool mouseMoveEvent(QMouseEvent* event); - void cleanupAllOverlays(); mutable QScriptEngine _scriptEngine; @@ -719,10 +714,6 @@ private: PointerEvent calculateOverlayPointerEvent(const QUuid& id, const PickRay& ray, const RayToOverlayIntersectionResult& rayPickResult, QMouseEvent* event, PointerEvent::EventType eventType); - unsigned int _mouseRayPickID; - QUuid _currentClickingOnOverlayID; - QUuid _currentHoverOverOverlayID; - static QString entityToOverlayType(const QString& type); static QString overlayToEntityType(const QString& type); static std::unordered_map _entityToOverlayTypes; @@ -730,15 +721,20 @@ private: QVariantMap convertEntityToOverlayProperties(const EntityItemProperties& entityProps); EntityItemProperties convertOverlayToEntityProperties(QVariantMap& overlayProps, const QString& type, bool add, const QUuid& id); - EntityItemProperties convertOverlayToEntityProperties(QVariantMap& overlayProps, glm::quat& rotationToSave, const QString& type, bool add, const QUuid& id = QUuid()); + EntityItemProperties convertOverlayToEntityProperties(QVariantMap& overlayProps, std::pair& rotationToSave, const QString& type, bool add, const QUuid& id = QUuid()); private slots: - void mousePressPointerEvent(const QUuid& id, const PointerEvent& event); - void mouseMovePointerEvent(const QUuid& id, const PointerEvent& event); + void mousePressOnPointerEvent(const QUuid& id, const PointerEvent& event); + void mousePressOffPointerEvent(); + void mouseDoublePressOnPointerEvent(const QUuid& id, const PointerEvent& event); + void mouseDoublePressOffPointerEvent(); void mouseReleasePointerEvent(const QUuid& id, const PointerEvent& event); + void mouseMovePointerEvent(const QUuid& id, const PointerEvent& event); void hoverEnterPointerEvent(const QUuid& id, const PointerEvent& event); void hoverOverPointerEvent(const QUuid& id, const PointerEvent& event); void hoverLeavePointerEvent(const QUuid& id, const PointerEvent& event); + + }; #define ADD_TYPE_MAP(entity, overlay) \ diff --git a/interface/src/ui/overlays/QmlOverlay.cpp b/interface/src/ui/overlays/QmlOverlay.cpp index 537c421ca7..f301a23d49 100644 --- a/interface/src/ui/overlays/QmlOverlay.cpp +++ b/interface/src/ui/overlays/QmlOverlay.cpp @@ -57,29 +57,15 @@ QmlOverlay::~QmlOverlay() { // QmlOverlay replaces Overlay's properties with those defined in the QML file used but keeps Overlay2D's properties. void QmlOverlay::setProperties(const QVariantMap& properties) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "setProperties", Q_ARG(QVariantMap, properties)); - return; - } - Overlay2D::setProperties(properties); - auto bounds = _bounds; + // check to see if qmlElement still exists if (_qmlElement) { - _qmlElement->setX(bounds.left()); - _qmlElement->setY(bounds.top()); - _qmlElement->setWidth(bounds.width()); - _qmlElement->setHeight(bounds.height()); + _qmlElement->setX(_bounds.left()); + _qmlElement->setY(_bounds.top()); + _qmlElement->setWidth(_bounds.width()); + _qmlElement->setHeight(_bounds.height()); + _qmlElement->setVisible(_visible); QMetaObject::invokeMethod(_qmlElement, "updatePropertiesFromScript", Qt::DirectConnection, Q_ARG(QVariant, properties)); } -} - -void QmlOverlay::render(RenderArgs* args) { - if (!_qmlElement) { - return; - } - - if (_visible != _qmlElement->isVisible()) { - _qmlElement->setVisible(_visible); - } -} +} \ No newline at end of file diff --git a/interface/src/ui/overlays/QmlOverlay.h b/interface/src/ui/overlays/QmlOverlay.h index 0951a04772..32badde28b 100644 --- a/interface/src/ui/overlays/QmlOverlay.h +++ b/interface/src/ui/overlays/QmlOverlay.h @@ -25,10 +25,8 @@ public: QmlOverlay(const QUrl& url, const QmlOverlay* overlay); ~QmlOverlay(); - bool supportsGetProperty() const override { return false; } - void setProperties(const QVariantMap& properties) override; - void render(RenderArgs* args) override; + void render(RenderArgs* args) override {} private: Q_INVOKABLE void qmlElementDestroyed(); diff --git a/libraries/animation/src/AnimClip.cpp b/libraries/animation/src/AnimClip.cpp index 1adc04ee1b..4fe02e9307 100644 --- a/libraries/animation/src/AnimClip.cpp +++ b/libraries/animation/src/AnimClip.cpp @@ -95,77 +95,139 @@ void AnimClip::setCurrentFrameInternal(float frame) { _frame = ::accumulateTime(_startFrame, _endFrame, _timeScale, frame + _startFrame, dt, _loopFlag, _id, triggers); } +static std::vector buildJointIndexMap(const AnimSkeleton& dstSkeleton, const AnimSkeleton& srcSkeleton) { + std::vector jointIndexMap; + int srcJointCount = srcSkeleton.getNumJoints(); + jointIndexMap.reserve(srcJointCount); + for (int srcJointIndex = 0; srcJointIndex < srcJointCount; srcJointIndex++) { + QString srcJointName = srcSkeleton.getJointName(srcJointIndex); + int dstJointIndex = dstSkeleton.nameToJointIndex(srcJointName); + jointIndexMap.push_back(dstJointIndex); + } + return jointIndexMap; +} + void AnimClip::copyFromNetworkAnim() { assert(_networkAnim && _networkAnim->isLoaded() && _skeleton); _anim.clear(); - // build a mapping from animation joint indices to skeleton joint indices. - // by matching joints with the same name. - const HFMModel& hfmModel = _networkAnim->getHFMModel(); - AnimSkeleton animSkeleton(hfmModel); - const auto animJointCount = animSkeleton.getNumJoints(); - const auto skeletonJointCount = _skeleton->getNumJoints(); - std::vector jointMap; - jointMap.reserve(animJointCount); - for (int i = 0; i < animJointCount; i++) { - int skeletonJoint = _skeleton->nameToJointIndex(animSkeleton.getJointName(i)); - jointMap.push_back(skeletonJoint); + auto avatarSkeleton = getSkeleton(); + const HFMModel& animModel = _networkAnim->getHFMModel(); + AnimSkeleton animSkeleton(animModel); + const int animJointCount = animSkeleton.getNumJoints(); + const int avatarJointCount = avatarSkeleton->getNumJoints(); + + // build a mapping from animation joint indices to avatar joint indices by matching joints with the same name. + std::vector avatarToAnimJointIndexMap = buildJointIndexMap(animSkeleton, *avatarSkeleton); + + const int animFrameCount = animModel.animationFrames.size(); + _anim.resize(animFrameCount); + + // find the size scale factor for translation in the animation. + const int avatarHipsParentIndex = avatarSkeleton->getParentIndex(avatarSkeleton->nameToJointIndex("Hips")); + const int animHipsParentIndex = animSkeleton.getParentIndex(animSkeleton.nameToJointIndex("Hips")); + const AnimPose& avatarHipsAbsoluteDefaultPose = avatarSkeleton->getAbsoluteDefaultPose(avatarSkeleton->nameToJointIndex("Hips")); + const AnimPose& animHipsAbsoluteDefaultPose = animSkeleton.getAbsoluteDefaultPose(animSkeleton.nameToJointIndex("Hips")); + + // the get the units and the heights for the animation and the avatar + const float avatarUnitScale = extractScale(avatarSkeleton->getGeometryOffset()).y; + const float animationUnitScale = extractScale(animModel.offset).y; + const float avatarHeightInMeters = avatarUnitScale * avatarHipsAbsoluteDefaultPose.trans().y; + const float animHeightInMeters = animationUnitScale * animHipsAbsoluteDefaultPose.trans().y; + + // get the parent scales for the avatar and the animation + float avatarHipsParentScale = 1.0f; + if (avatarHipsParentIndex >= 0) { + const AnimPose& avatarHipsParentAbsoluteDefaultPose = avatarSkeleton->getAbsoluteDefaultPose(avatarHipsParentIndex); + avatarHipsParentScale = avatarHipsParentAbsoluteDefaultPose.scale().y; + } + float animHipsParentScale = 1.0f; + if (animHipsParentIndex >= 0) { + const AnimPose& animationHipsParentAbsoluteDefaultPose = animSkeleton.getAbsoluteDefaultPose(animHipsParentIndex); + animHipsParentScale = animationHipsParentAbsoluteDefaultPose.scale().y; } - const int frameCount = hfmModel.animationFrames.size(); - _anim.resize(frameCount); + const float EPSILON = 0.0001f; + float boneLengthScale = 1.0f; + // compute the ratios for the units, the heights in meters, and the parent scales + if ((fabsf(animHeightInMeters) > EPSILON) && (animationUnitScale > EPSILON) && (animHipsParentScale > EPSILON)) { + const float avatarToAnimationHeightRatio = avatarHeightInMeters / animHeightInMeters; + const float unitsRatio = 1.0f / (avatarUnitScale / animationUnitScale); + const float parentScaleRatio = 1.0f / (avatarHipsParentScale / animHipsParentScale); - for (int frame = 0; frame < frameCount; frame++) { + boneLengthScale = avatarToAnimationHeightRatio * unitsRatio * parentScaleRatio; + } - const HFMAnimationFrame& hfmAnimFrame = hfmModel.animationFrames[frame]; - // init all joints in animation to default pose - // this will give us a resonable result for bones in the model skeleton but not in the animation. - _anim[frame].reserve(skeletonJointCount); - for (int skeletonJoint = 0; skeletonJoint < skeletonJointCount; skeletonJoint++) { - _anim[frame].push_back(_skeleton->getRelativeDefaultPose(skeletonJoint)); + for (int frame = 0; frame < animFrameCount; frame++) { + const HFMAnimationFrame& animFrame = animModel.animationFrames[frame]; + + // extract the full rotations from the animFrame (including pre and post rotations from the animModel). + std::vector animRotations; + animRotations.reserve(animJointCount); + for (int i = 0; i < animJointCount; i++) { + animRotations.push_back(animModel.joints[i].preRotation * animFrame.rotations[i] * animModel.joints[i].postRotation); } - for (int animJoint = 0; animJoint < animJointCount; animJoint++) { - int skeletonJoint = jointMap[animJoint]; + // convert rotations into absolute frame + animSkeleton.convertRelativeRotationsToAbsolute(animRotations); - const glm::vec3& hfmAnimTrans = hfmAnimFrame.translations[animJoint]; - const glm::quat& hfmAnimRot = hfmAnimFrame.rotations[animJoint]; - - // skip joints that are in the animation but not in the skeleton. - if (skeletonJoint >= 0 && skeletonJoint < skeletonJointCount) { - - AnimPose preRot, postRot; - preRot = animSkeleton.getPreRotationPose(animJoint); - postRot = animSkeleton.getPostRotationPose(animJoint); - - // cancel out scale - preRot.scale() = glm::vec3(1.0f); - postRot.scale() = glm::vec3(1.0f); - - AnimPose rot(glm::vec3(1.0f), hfmAnimRot, glm::vec3()); - - // adjust translation offsets, so large translation animatons on the reference skeleton - // will be adjusted when played on a skeleton with short limbs. - const glm::vec3& hfmZeroTrans = hfmModel.animationFrames[0].translations[animJoint]; - const AnimPose& relDefaultPose = _skeleton->getRelativeDefaultPose(skeletonJoint); - float boneLengthScale = 1.0f; - const float EPSILON = 0.0001f; - if (fabsf(glm::length(hfmZeroTrans)) > EPSILON) { - boneLengthScale = glm::length(relDefaultPose.trans()) / glm::length(hfmZeroTrans); + // build absolute rotations for the avatar + std::vector avatarRotations; + avatarRotations.reserve(avatarJointCount); + for (int avatarJointIndex = 0; avatarJointIndex < avatarJointCount; avatarJointIndex++) { + int animJointIndex = avatarToAnimJointIndexMap[avatarJointIndex]; + if (animJointIndex >= 0) { + // This joint is in both animation and avatar. + // Set the absolute rotation directly + avatarRotations.push_back(animRotations[animJointIndex]); + } else { + // This joint is NOT in the animation at all. + // Set it so that the default relative rotation remains unchanged. + glm::quat avatarRelativeDefaultRot = avatarSkeleton->getRelativeDefaultPose(avatarJointIndex).rot(); + glm::quat avatarParentAbsoluteRot; + int avatarParentJointIndex = avatarSkeleton->getParentIndex(avatarJointIndex); + if (avatarParentJointIndex >= 0) { + avatarParentAbsoluteRot = avatarRotations[avatarParentJointIndex]; } - - AnimPose trans = AnimPose(glm::vec3(1.0f), glm::quat(), relDefaultPose.trans() + boneLengthScale * (hfmAnimTrans - hfmZeroTrans)); - - _anim[frame][skeletonJoint] = trans * preRot * rot * postRot; + avatarRotations.push_back(avatarParentAbsoluteRot * avatarRelativeDefaultRot); } } + + // convert avatar rotations into relative frame + avatarSkeleton->convertAbsoluteRotationsToRelative(avatarRotations); + + _anim[frame].reserve(avatarJointCount); + for (int avatarJointIndex = 0; avatarJointIndex < avatarJointCount; avatarJointIndex++) { + const AnimPose& avatarDefaultPose = avatarSkeleton->getRelativeDefaultPose(avatarJointIndex); + + // copy scale over from avatar default pose + glm::vec3 relativeScale = avatarDefaultPose.scale(); + + glm::vec3 relativeTranslation; + int animJointIndex = avatarToAnimJointIndexMap[avatarJointIndex]; + if (animJointIndex >= 0) { + // This joint is in both animation and avatar. + const glm::vec3& animTrans = animFrame.translations[animJointIndex]; + + // retarget translation from animation to avatar + const glm::vec3& animZeroTrans = animModel.animationFrames[0].translations[animJointIndex]; + relativeTranslation = avatarDefaultPose.trans() + boneLengthScale * (animTrans - animZeroTrans); + } else { + // This joint is NOT in the animation at all. + // preserve the default translation. + relativeTranslation = avatarDefaultPose.trans(); + } + + // build the final pose + _anim[frame].push_back(AnimPose(relativeScale, avatarRotations[avatarJointIndex], relativeTranslation)); + } } // mirrorAnim will be re-built on demand, if needed. _mirrorAnim.clear(); - _poses.resize(skeletonJointCount); + _poses.resize(avatarJointCount); } void AnimClip::buildMirrorAnim() { diff --git a/libraries/animation/src/AnimContext.h b/libraries/animation/src/AnimContext.h index c455dd9c8f..e3ab5d9788 100644 --- a/libraries/animation/src/AnimContext.h +++ b/libraries/animation/src/AnimContext.h @@ -28,6 +28,7 @@ enum class AnimNodeType { InverseKinematics, DefaultPose, TwoBoneIK, + SplineIK, PoleVectorConstraint, NumTypes }; diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index a1809f3438..ed3d5d67d6 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -298,10 +298,8 @@ void AnimInverseKinematics::solve(const AnimContext& context, const std::vector< } // harvest accumulated rotations and apply the average - for (int i = 0; i < (int)_relativePoses.size(); ++i) { - if (i == _hipsIndex) { - continue; // don't apply accumulators to hips - } + // don't apply accumulators to hips, or parents of hips + for (int i = (_hipsIndex+1); i < (int)_relativePoses.size(); ++i) { if (_rotationAccumulators[i].size() > 0) { _relativePoses[i].rot() = _rotationAccumulators[i].getAverage(); _rotationAccumulators[i].clear(); @@ -865,11 +863,6 @@ const AnimPoseVec& AnimInverseKinematics::evaluate(const AnimVariantMap& animVar //virtual const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars, const AnimContext& context, float dt, AnimVariantMap& triggersOut, const AnimPoseVec& underPoses) { -#ifdef Q_OS_ANDROID - // disable IK on android - return underPoses; -#endif - // allows solutionSource to be overridden by an animVar auto solutionSource = animVars.lookup(_solutionSourceVar, (int)_solutionSource); diff --git a/libraries/animation/src/AnimNodeLoader.cpp b/libraries/animation/src/AnimNodeLoader.cpp index dfa61e9fea..b637d131f8 100644 --- a/libraries/animation/src/AnimNodeLoader.cpp +++ b/libraries/animation/src/AnimNodeLoader.cpp @@ -26,6 +26,7 @@ #include "AnimInverseKinematics.h" #include "AnimDefaultPose.h" #include "AnimTwoBoneIK.h" +#include "AnimSplineIK.h" #include "AnimPoleVectorConstraint.h" using NodeLoaderFunc = AnimNode::Pointer (*)(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); @@ -41,6 +42,7 @@ static AnimNode::Pointer loadManipulatorNode(const QJsonObject& jsonObj, const Q static AnimNode::Pointer loadInverseKinematicsNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); static AnimNode::Pointer loadDefaultPoseNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); static AnimNode::Pointer loadTwoBoneIKNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); +static AnimNode::Pointer loadSplineIKNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); static AnimNode::Pointer loadPoleVectorConstraintNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); static const float ANIM_GRAPH_LOAD_PRIORITY = 10.0f; @@ -61,6 +63,7 @@ static const char* animNodeTypeToString(AnimNode::Type type) { case AnimNode::Type::InverseKinematics: return "inverseKinematics"; case AnimNode::Type::DefaultPose: return "defaultPose"; case AnimNode::Type::TwoBoneIK: return "twoBoneIK"; + case AnimNode::Type::SplineIK: return "splineIK"; case AnimNode::Type::PoleVectorConstraint: return "poleVectorConstraint"; case AnimNode::Type::NumTypes: return nullptr; }; @@ -123,6 +126,7 @@ static NodeLoaderFunc animNodeTypeToLoaderFunc(AnimNode::Type type) { case AnimNode::Type::InverseKinematics: return loadInverseKinematicsNode; case AnimNode::Type::DefaultPose: return loadDefaultPoseNode; case AnimNode::Type::TwoBoneIK: return loadTwoBoneIKNode; + case AnimNode::Type::SplineIK: return loadSplineIKNode; case AnimNode::Type::PoleVectorConstraint: return loadPoleVectorConstraintNode; case AnimNode::Type::NumTypes: return nullptr; }; @@ -140,6 +144,7 @@ static NodeProcessFunc animNodeTypeToProcessFunc(AnimNode::Type type) { case AnimNode::Type::InverseKinematics: return processDoNothing; case AnimNode::Type::DefaultPose: return processDoNothing; case AnimNode::Type::TwoBoneIK: return processDoNothing; + case AnimNode::Type::SplineIK: return processDoNothing; case AnimNode::Type::PoleVectorConstraint: return processDoNothing; case AnimNode::Type::NumTypes: return nullptr; }; @@ -574,6 +579,52 @@ static AnimNode::Pointer loadDefaultPoseNode(const QJsonObject& jsonObj, const Q return node; } +static AnimNode::Pointer loadSplineIKNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { + READ_FLOAT(alpha, jsonObj, id, jsonUrl, nullptr); + READ_BOOL(enabled, jsonObj, id, jsonUrl, nullptr); + READ_FLOAT(interpDuration, jsonObj, id, jsonUrl, nullptr); + READ_STRING(baseJointName, jsonObj, id, jsonUrl, nullptr); + READ_STRING(midJointName, jsonObj, id, jsonUrl, nullptr); + READ_STRING(tipJointName, jsonObj, id, jsonUrl, nullptr); + READ_STRING(basePositionVar, jsonObj, id, jsonUrl, nullptr); + READ_STRING(baseRotationVar, jsonObj, id, jsonUrl, nullptr); + READ_STRING(midPositionVar, jsonObj, id, jsonUrl, nullptr); + READ_STRING(midRotationVar, jsonObj, id, jsonUrl, nullptr); + READ_STRING(tipPositionVar, jsonObj, id, jsonUrl, nullptr); + READ_STRING(tipRotationVar, jsonObj, id, jsonUrl, nullptr); + READ_STRING(alphaVar, jsonObj, id, jsonUrl, nullptr); + READ_STRING(enabledVar, jsonObj, id, jsonUrl, nullptr); + + auto tipFlexCoefficientsValue = jsonObj.value("tipTargetFlexCoefficients"); + if (!tipFlexCoefficientsValue.isArray()) { + qCCritical(animation) << "AnimNodeLoader, bad or missing tip flex array"; + return nullptr; + } + auto tipFlexCoefficientsArray = tipFlexCoefficientsValue.toArray(); + std::vector tipTargetFlexCoefficients; + for (const auto& value : tipFlexCoefficientsArray) { + tipTargetFlexCoefficients.push_back((float)value.toDouble()); + } + + auto midFlexCoefficientsValue = jsonObj.value("midTargetFlexCoefficients"); + if (!midFlexCoefficientsValue.isArray()) { + qCCritical(animation) << "AnimNodeLoader, bad or missing mid flex array"; + return nullptr; + } + auto midFlexCoefficientsArray = midFlexCoefficientsValue.toArray(); + std::vector midTargetFlexCoefficients; + for (const auto& midValue : midFlexCoefficientsArray) { + midTargetFlexCoefficients.push_back((float)midValue.toDouble()); + } + + auto node = std::make_shared(id, alpha, enabled, interpDuration, + baseJointName, midJointName, tipJointName, + basePositionVar, baseRotationVar, midPositionVar, midRotationVar, + tipPositionVar, tipRotationVar, alphaVar, enabledVar, + tipTargetFlexCoefficients, midTargetFlexCoefficients); + return node; +} + static AnimNode::Pointer loadTwoBoneIKNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { READ_FLOAT(alpha, jsonObj, id, jsonUrl, nullptr); READ_BOOL(enabled, jsonObj, id, jsonUrl, nullptr); diff --git a/libraries/animation/src/AnimPoleVectorConstraint.cpp b/libraries/animation/src/AnimPoleVectorConstraint.cpp index f017fe2348..c0600ee253 100644 --- a/libraries/animation/src/AnimPoleVectorConstraint.cpp +++ b/libraries/animation/src/AnimPoleVectorConstraint.cpp @@ -117,7 +117,7 @@ const AnimPoseVec& AnimPoleVectorConstraint::evaluate(const AnimVariantMap& anim if (axisLength > MIN_LENGTH && refVectorLength > MIN_LENGTH && sideVectorLength > MIN_LENGTH && refVectorProjLength > MIN_LENGTH && poleVectorProjLength > MIN_LENGTH) { - float dot = glm::clamp(glm::dot(refVectorProj / refVectorProjLength, poleVectorProj / poleVectorProjLength), 0.0f, 1.0f); + float dot = glm::clamp(glm::dot(refVectorProj / refVectorProjLength, poleVectorProj / poleVectorProjLength), -1.0f, 1.0f); float sideDot = glm::dot(poleVector, sideVector); float theta = copysignf(1.0f, sideDot) * acosf(dot); diff --git a/libraries/animation/src/AnimPose.cpp b/libraries/animation/src/AnimPose.cpp index d77514e691..8649db8233 100644 --- a/libraries/animation/src/AnimPose.cpp +++ b/libraries/animation/src/AnimPose.cpp @@ -11,7 +11,6 @@ #include "AnimPose.h" #include #include -#include #include "AnimUtil.h" const AnimPose AnimPose::identity = AnimPose(glm::vec3(1.0f), @@ -19,16 +18,29 @@ const AnimPose AnimPose::identity = AnimPose(glm::vec3(1.0f), glm::vec3(0.0f)); AnimPose::AnimPose(const glm::mat4& mat) { - static const float EPSILON = 0.0001f; - _scale = extractScale(mat); - // quat_cast doesn't work so well with scaled matrices, so cancel it out. - glm::mat4 tmp = glm::scale(mat, 1.0f / _scale); + glm::mat3 m(mat); + _scale = glm::vec3(glm::length(m[0]), glm::length(m[1]), glm::length(m[2])); + float det = glm::determinant(m); + + glm::mat3 tmp; + if (det < 0.0f) { + _scale *= -1.0f; + } + + // quat_cast doesn't work so well with scaled matrices, so cancel out scale. + // also, as a side effect, multiply mirrored matrices by -1 to get the right rotation out. + tmp[0] = m[0] * (1.0f / _scale[0]); + tmp[1] = m[1] * (1.0f / _scale[1]); + tmp[2] = m[2] * (1.0f / _scale[2]); _rot = glm::quat_cast(tmp); + + // normalize quat if necessary float lengthSquared = glm::length2(_rot); if (glm::abs(lengthSquared - 1.0f) > EPSILON) { float oneOverLength = 1.0f / sqrtf(lengthSquared); _rot = glm::quat(_rot.w * oneOverLength, _rot.x * oneOverLength, _rot.y * oneOverLength, _rot.z * oneOverLength); } + _trans = extractTranslation(mat); } diff --git a/libraries/animation/src/AnimSkeleton.cpp b/libraries/animation/src/AnimSkeleton.cpp index 03e3ac6ebd..b26d00d8d0 100644 --- a/libraries/animation/src/AnimSkeleton.cpp +++ b/libraries/animation/src/AnimSkeleton.cpp @@ -17,6 +17,9 @@ #include "AnimationLogging.h" AnimSkeleton::AnimSkeleton(const HFMModel& hfmModel) { + + _geometryOffset = hfmModel.offset; + // convert to std::vector of joints std::vector joints; joints.reserve(hfmModel.joints.size()); @@ -149,8 +152,19 @@ void AnimSkeleton::convertAbsolutePosesToRelative(AnimPoseVec& poses) const { } } +void AnimSkeleton::convertRelativeRotationsToAbsolute(std::vector& rotations) const { + // rotations start off relative and leave in absolute frame + int lastIndex = std::min((int)rotations.size(), _jointsSize); + for (int i = 0; i < lastIndex; ++i) { + int parentIndex = _parentIndices[i]; + if (parentIndex != -1) { + rotations[i] = rotations[parentIndex] * rotations[i]; + } + } +} + void AnimSkeleton::convertAbsoluteRotationsToRelative(std::vector& rotations) const { - // poses start off absolute and leave in relative frame + // rotations start off absolute and leave in relative frame int lastIndex = std::min((int)rotations.size(), _jointsSize); for (int i = lastIndex - 1; i >= 0; --i) { int parentIndex = _parentIndices[i]; diff --git a/libraries/animation/src/AnimSkeleton.h b/libraries/animation/src/AnimSkeleton.h index 0eefbf973e..efc1c1599f 100644 --- a/libraries/animation/src/AnimSkeleton.h +++ b/libraries/animation/src/AnimSkeleton.h @@ -36,6 +36,7 @@ public: const AnimPoseVec& getRelativeDefaultPoses() const { return _relativeDefaultPoses; } const AnimPose& getAbsoluteDefaultPose(int jointIndex) const; const AnimPoseVec& getAbsoluteDefaultPoses() const { return _absoluteDefaultPoses; } + const glm::mat4& getGeometryOffset() const { return _geometryOffset; } // get pre transform which should include FBX pre potations const AnimPose& getPreRotationPose(int jointIndex) const; @@ -54,6 +55,7 @@ public: void convertRelativePosesToAbsolute(AnimPoseVec& poses) const; void convertAbsolutePosesToRelative(AnimPoseVec& poses) const; + void convertRelativeRotationsToAbsolute(std::vector& rotations) const; void convertAbsoluteRotationsToRelative(std::vector& rotations) const; void saveNonMirroredPoses(const AnimPoseVec& poses) const; @@ -83,6 +85,7 @@ protected: std::vector _mirrorMap; QHash _jointIndicesByName; std::vector> _clusterBindMatrixOriginalValues; + glm::mat4 _geometryOffset; // no copies AnimSkeleton(const AnimSkeleton&) = delete; diff --git a/libraries/animation/src/AnimSplineIK.cpp b/libraries/animation/src/AnimSplineIK.cpp new file mode 100644 index 0000000000..cfb34560ff --- /dev/null +++ b/libraries/animation/src/AnimSplineIK.cpp @@ -0,0 +1,473 @@ +// +// AnimSplineIK.cpp +// +// Created by Angus Antley on 1/7/19. +// Copyright (c) 2019 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "AnimSplineIK.h" +#include "AnimationLogging.h" +#include "CubicHermiteSpline.h" +#include +#include "AnimUtil.h" + +static const float FRAMES_PER_SECOND = 30.0f; + +AnimSplineIK::AnimSplineIK(const QString& id, float alpha, bool enabled, float interpDuration, + const QString& baseJointName, + const QString& midJointName, + const QString& tipJointName, + const QString& basePositionVar, + const QString& baseRotationVar, + const QString& midPositionVar, + const QString& midRotationVar, + const QString& tipPositionVar, + const QString& tipRotationVar, + const QString& alphaVar, + const QString& enabledVar, + const std::vector tipTargetFlexCoefficients, + const std::vector midTargetFlexCoefficients) : + AnimNode(AnimNode::Type::SplineIK, id), + _alpha(alpha), + _enabled(enabled), + _interpDuration(interpDuration), + _baseJointName(baseJointName), + _midJointName(midJointName), + _tipJointName(tipJointName), + _basePositionVar(basePositionVar), + _baseRotationVar(baseRotationVar), + _midPositionVar(midPositionVar), + _midRotationVar(midRotationVar), + _tipPositionVar(tipPositionVar), + _tipRotationVar(tipRotationVar), + _alphaVar(alphaVar), + _enabledVar(enabledVar) +{ + + for (int i = 0; i < (int)tipTargetFlexCoefficients.size(); i++) { + if (i < MAX_NUMBER_FLEX_VARIABLES) { + _tipTargetFlexCoefficients[i] = tipTargetFlexCoefficients[i]; + } + } + _numTipTargetFlexCoefficients = std::min((int)tipTargetFlexCoefficients.size(), MAX_NUMBER_FLEX_VARIABLES); + + for (int i = 0; i < (int)midTargetFlexCoefficients.size(); i++) { + if (i < MAX_NUMBER_FLEX_VARIABLES) { + _midTargetFlexCoefficients[i] = midTargetFlexCoefficients[i]; + } + } + _numMidTargetFlexCoefficients = std::min((int)midTargetFlexCoefficients.size(), MAX_NUMBER_FLEX_VARIABLES); + +} + +AnimSplineIK::~AnimSplineIK() { + +} + +const AnimPoseVec& AnimSplineIK::evaluate(const AnimVariantMap& animVars, const AnimContext& context, float dt, AnimVariantMap& triggersOut) { + assert(_children.size() == 1); + if (_children.size() != 1) { + return _poses; + } + + const float MIN_ALPHA = 0.0f; + const float MAX_ALPHA = 1.0f; + float alpha = glm::clamp(animVars.lookup(_alphaVar, _alpha), MIN_ALPHA, MAX_ALPHA); + + // evaluate underPoses + AnimPoseVec underPoses = _children[0]->evaluate(animVars, context, dt, triggersOut); + + // if we don't have a skeleton, or jointName lookup failed or the spline alpha is 0 or there are no underposes. + if (!_skeleton || _baseJointIndex == -1 || _midJointIndex == -1 || _tipJointIndex == -1 || alpha < EPSILON || underPoses.size() == 0) { + // pass underPoses through unmodified. + _poses = underPoses; + return _poses; + } + + // guard against size change + if (underPoses.size() != _poses.size()) { + _poses = underPoses; + } + + // determine if we should interpolate + bool enabled = animVars.lookup(_enabledVar, _enabled); + if (enabled != _enabled) { + AnimChain poseChain; + poseChain.buildFromRelativePoses(_skeleton, _poses, _tipJointIndex); + if (enabled) { + beginInterp(InterpType::SnapshotToSolve, poseChain); + } else { + beginInterp(InterpType::SnapshotToUnderPoses, poseChain); + } + } + _enabled = enabled; + + // now that we have saved the previous _poses in _snapshotChain, we can update to the current underposes + _poses = underPoses; + + // don't build chains or do IK if we are disabled & not interping. + if (_interpType == InterpType::None && !enabled) { + return _poses; + } + + // compute under chain for possible interpolation + AnimChain underChain; + underChain.buildFromRelativePoses(_skeleton, underPoses, _tipJointIndex); + + AnimPose baseTargetAbsolutePose; + // if there is a baseJoint ik target in animvars then set the joint to that + // otherwise use the underpose + AnimPose baseJointUnderPose = _skeleton->getAbsolutePose(_baseJointIndex, _poses); + baseTargetAbsolutePose.rot() = animVars.lookupRigToGeometry(_baseRotationVar, baseJointUnderPose.rot()); + baseTargetAbsolutePose.trans() = animVars.lookupRigToGeometry(_basePositionVar, baseJointUnderPose.trans()); + + int baseParentIndex = _skeleton->getParentIndex(_baseJointIndex); + AnimPose baseParentAbsPose(Quaternions::IDENTITY,glm::vec3()); + if (baseParentIndex >= 0) { + baseParentAbsPose = _skeleton->getAbsolutePose(baseParentIndex, _poses); + } + _poses[_baseJointIndex] = baseParentAbsPose.inverse() * baseTargetAbsolutePose; + _poses[_baseJointIndex].scale() = glm::vec3(1.0f); + + // initialize the middle joint target + IKTarget midTarget; + midTarget.setType((int)IKTarget::Type::Spline); + midTarget.setIndex(_midJointIndex); + AnimPose absPoseMid = _skeleton->getAbsolutePose(_midJointIndex, _poses); + glm::quat midTargetRotation = animVars.lookupRigToGeometry(_midRotationVar, absPoseMid.rot()); + glm::vec3 midTargetPosition = animVars.lookupRigToGeometry(_midPositionVar, absPoseMid.trans()); + midTarget.setPose(midTargetRotation, midTargetPosition); + midTarget.setWeight(1.0f); + midTarget.setFlexCoefficients(_numMidTargetFlexCoefficients, _midTargetFlexCoefficients); + + // solve the lower spine spline + AnimChain midJointChain; + AnimPoseVec absolutePosesAfterBaseTipSpline; + absolutePosesAfterBaseTipSpline.resize(_poses.size()); + computeAbsolutePoses(absolutePosesAfterBaseTipSpline); + midJointChain.buildFromRelativePoses(_skeleton, _poses, midTarget.getIndex()); + solveTargetWithSpline(context, _baseJointIndex, midTarget, absolutePosesAfterBaseTipSpline, context.getEnableDebugDrawIKChains(), midJointChain); + midJointChain.outputRelativePoses(_poses); + + // initialize the tip target + IKTarget tipTarget; + tipTarget.setType((int)IKTarget::Type::Spline); + tipTarget.setIndex(_tipJointIndex); + AnimPose absPoseTip = _skeleton->getAbsolutePose(_tipJointIndex, _poses); + glm::quat tipRotation = animVars.lookupRigToGeometry(_tipRotationVar, absPoseTip.rot()); + glm::vec3 tipTranslation = animVars.lookupRigToGeometry(_tipPositionVar, absPoseTip.trans()); + tipTarget.setPose(tipRotation, tipTranslation); + tipTarget.setWeight(1.0f); + tipTarget.setFlexCoefficients(_numTipTargetFlexCoefficients, _tipTargetFlexCoefficients); + + // solve the upper spine spline + AnimChain upperJointChain; + AnimPoseVec finalAbsolutePoses; + finalAbsolutePoses.resize(_poses.size()); + computeAbsolutePoses(finalAbsolutePoses); + upperJointChain.buildFromRelativePoses(_skeleton, _poses, tipTarget.getIndex()); + solveTargetWithSpline(context, _midJointIndex, tipTarget, finalAbsolutePoses, context.getEnableDebugDrawIKChains(), upperJointChain); + upperJointChain.buildDirtyAbsolutePoses(); + upperJointChain.outputRelativePoses(_poses); + + // compute chain + AnimChain ikChain; + ikChain.buildFromRelativePoses(_skeleton, _poses, _tipJointIndex); + // blend with the underChain + ikChain.blend(underChain, alpha); + + // apply smooth interpolation when turning ik on and off + if (_interpType != InterpType::None) { + _interpAlpha += _interpAlphaVel * dt; + + // ease in expo + float easeInAlpha = 1.0f - powf(2.0f, -10.0f * _interpAlpha); + + if (_interpAlpha < 1.0f) { + AnimChain interpChain; + if (_interpType == InterpType::SnapshotToUnderPoses) { + interpChain = underChain; + interpChain.blend(_snapshotChain, easeInAlpha); + } else if (_interpType == InterpType::SnapshotToSolve) { + interpChain = ikChain; + interpChain.blend(_snapshotChain, easeInAlpha); + } + // copy interpChain into _poses + interpChain.outputRelativePoses(_poses); + } else { + // interpolation complete + _interpType = InterpType::None; + } + } + + if (_interpType == InterpType::None) { + if (enabled) { + // copy chain into _poses + ikChain.outputRelativePoses(_poses); + } else { + // copy under chain into _poses + underChain.outputRelativePoses(_poses); + } + } + + // debug render ik targets + if (context.getEnableDebugDrawIKTargets()) { + const vec4 WHITE(1.0f); + const vec4 GREEN(0.0f, 1.0f, 0.0f, 1.0f); + glm::mat4 rigToAvatarMat = createMatFromQuatAndPos(Quaternions::Y_180, glm::vec3()); + + glm::mat4 geomTargetMat = createMatFromQuatAndPos(tipTarget.getRotation(), tipTarget.getTranslation()); + glm::mat4 avatarTargetMat = rigToAvatarMat * context.getGeometryToRigMatrix() * geomTargetMat; + QString name = QString("ikTargetSplineTip"); + DebugDraw::getInstance().addMyAvatarMarker(name, glmExtractRotation(avatarTargetMat), extractTranslation(avatarTargetMat), WHITE); + + glm::mat4 geomTargetMat2 = createMatFromQuatAndPos(midTarget.getRotation(), midTarget.getTranslation()); + glm::mat4 avatarTargetMat2 = rigToAvatarMat * context.getGeometryToRigMatrix() * geomTargetMat2; + QString name2 = QString("ikTargetSplineMid"); + DebugDraw::getInstance().addMyAvatarMarker(name2, glmExtractRotation(avatarTargetMat2), extractTranslation(avatarTargetMat2), WHITE); + + glm::mat4 geomTargetMat3 = createMatFromQuatAndPos(baseTargetAbsolutePose.rot(), baseTargetAbsolutePose.trans()); + glm::mat4 avatarTargetMat3 = rigToAvatarMat * context.getGeometryToRigMatrix() * geomTargetMat3; + QString name3 = QString("ikTargetSplineBase"); + DebugDraw::getInstance().addMyAvatarMarker(name3, glmExtractRotation(avatarTargetMat3), extractTranslation(avatarTargetMat3), WHITE); + + + } else if (context.getEnableDebugDrawIKTargets() != _previousEnableDebugIKTargets) { + + // remove markers if they were added last frame. + QString name = QString("ikTargetSplineTip"); + DebugDraw::getInstance().removeMyAvatarMarker(name); + QString name2 = QString("ikTargetSplineMid"); + DebugDraw::getInstance().removeMyAvatarMarker(name2); + QString name3 = QString("ikTargetSplineBase"); + DebugDraw::getInstance().removeMyAvatarMarker(name3); + } + _previousEnableDebugIKTargets = context.getEnableDebugDrawIKTargets(); + + return _poses; +} + +void AnimSplineIK::lookUpIndices() { + assert(_skeleton); + + // look up bone indices by name + std::vector indices = _skeleton->lookUpJointIndices({ _baseJointName, _tipJointName, _midJointName }); + + // cache the results + _baseJointIndex = indices[0]; + _tipJointIndex = indices[1]; + _midJointIndex = indices[2]; +} + +void AnimSplineIK::computeAbsolutePoses(AnimPoseVec& absolutePoses) const { + int numJoints = (int)_poses.size(); + assert(numJoints <= _skeleton->getNumJoints()); + assert(numJoints == (int)absolutePoses.size()); + for (int i = 0; i < numJoints; ++i) { + int parentIndex = _skeleton->getParentIndex(i); + if (parentIndex < 0) { + absolutePoses[i] = _poses[i]; + } else { + absolutePoses[i] = absolutePoses[parentIndex] * _poses[i]; + } + } +} + +// for AnimDebugDraw rendering +const AnimPoseVec& AnimSplineIK::getPosesInternal() const { + return _poses; +} + +void AnimSplineIK::setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) { + AnimNode::setSkeletonInternal(skeleton); + lookUpIndices(); +} + +void AnimSplineIK::solveTargetWithSpline(const AnimContext& context, int base, const IKTarget& target, const AnimPoseVec& absolutePoses, bool debug, AnimChain& chainInfoOut) const { + + // build spline from tip to base + AnimPose tipPose = AnimPose(glm::vec3(1.0f), target.getRotation(), target.getTranslation()); + AnimPose basePose = absolutePoses[base]; + + CubicHermiteSplineFunctorWithArcLength spline; + if (target.getIndex() == _tipJointIndex) { + // 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 = CubicHermiteSplineFunctorWithArcLength(tipPose.rot(), tipPose.trans(), basePose.rot(), basePose.trans(), HIPS_GAIN, HEAD_GAIN); + } else { + spline = CubicHermiteSplineFunctorWithArcLength(tipPose.rot(),tipPose.trans(), basePose.rot(), basePose.trans()); + } + 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 = safeLerp(basePose.rot(), tipPose.rot(), 0.5f); + if (glm::dot(halfRot * Vectors::UNIT_Z, basePose.rot() * Vectors::UNIT_Z) < 0.0f) { + tipPose.rot() = -tipPose.rot(); + } + + // find or create splineJointInfo for this target + const std::vector* splineJointInfoVec = findOrCreateSplineJointInfo(context, base, target); + + if (splineJointInfoVec && splineJointInfoVec->size() > 0) { + const int baseParentIndex = _skeleton->getParentIndex(base); + 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 base->tip splines, preform most twist toward the tip by using ease in function. t^2 + float rotT = t; + if (target.getIndex() == _tipJointIndex) { + rotT = t * t; + } + glm::quat twistRot = safeLerp(basePose.rot(), tipPose.rot(), rotT); + + // compute the rotation by using the derivative of the spline as the y-axis, and the twistRot x-axis + glm::vec3 y = glm::normalize(spline.d(t)); + 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; + // get the number of flex coeff for this spline + float interpedCoefficient = 1.0f; + int numFlexCoeff = target.getNumFlexCoefficients(); + if (numFlexCoeff == (int)splineJointInfoVec->size()) { + // then do nothing special + interpedCoefficient = target.getFlexCoefficient(i); + } else { + // interp based on ratio of the joint. + if (splineJointInfo.ratio < 1.0f) { + float flexInterp = splineJointInfo.ratio * (float)(numFlexCoeff - 1); + int startCoeff = (int)glm::floor(flexInterp); + float partial = flexInterp - startCoeff; + interpedCoefficient = target.getFlexCoefficient(startCoeff) * (1.0f - partial) + target.getFlexCoefficient(startCoeff + 1) * partial; + } else { + interpedCoefficient = target.getFlexCoefficient(numFlexCoeff - 1); + } + } + ::blend(1, &absolutePoses[splineJointInfo.jointIndex], &desiredAbsPose, interpedCoefficient, &flexedAbsPose); + + AnimPose relPose = parentAbsPose.inverse() * flexedAbsPose; + + if (splineJointInfo.jointIndex != base) { + // 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; + } else if (length < MIN_LENGTH) { + relPose.trans() = (relPose.trans() / length) * MIN_LENGTH; + } + } else { + relPose.trans() = glm::vec3(0.0f); + } + } + + if (!chainInfoOut.setRelativePoseAtJointIndex(splineJointInfo.jointIndex, relPose)) { + qCDebug(animation) << "error: joint not found in spline chain"; + } + + parentAbsPose = flexedAbsPose; + } + } + + if (debug) { + const vec4 CYAN(0.0f, 1.0f, 1.0f, 1.0f); + chainInfoOut.debugDraw(context.getRigToWorldMatrix() * context.getGeometryToRigMatrix(), CYAN); + } +} + +const std::vector* AnimSplineIK::findOrCreateSplineJointInfo(const AnimContext& context, int base, const IKTarget& target) const { + // find or create splineJointInfo for this target + auto iter = _splineJointInfoMap.find(target.getIndex()); + if (iter != _splineJointInfoMap.end()) { + return &(iter->second); + } else { + computeAndCacheSplineJointInfosForIKTarget(context, base, target); + auto iter = _splineJointInfoMap.find(target.getIndex()); + if (iter != _splineJointInfoMap.end()) { + return &(iter->second); + } + } + return nullptr; +} + +// pre-compute information about each joint influenced by this spline IK target. +void AnimSplineIK::computeAndCacheSplineJointInfosForIKTarget(const AnimContext& context, int base, const IKTarget& target) const { + std::vector splineJointInfoVec; + + // build spline between the default poses. + AnimPose tipPose = _skeleton->getAbsoluteDefaultPose(target.getIndex()); + AnimPose basePose = _skeleton->getAbsoluteDefaultPose(base); + + CubicHermiteSplineFunctorWithArcLength spline; + if (target.getIndex() == _tipJointIndex) { + // 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 = CubicHermiteSplineFunctorWithArcLength(tipPose.rot(), tipPose.trans(), basePose.rot(), basePose.trans(), HIPS_GAIN, HEAD_GAIN); + } else { + spline = CubicHermiteSplineFunctorWithArcLength(tipPose.rot(), tipPose.trans(), basePose.rot(), basePose.trans()); + } + // 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(base); + + while (index != endIndex) { + AnimPose defaultPose = _skeleton->getAbsoluteDefaultPose(index); + glm::vec3 baseToCurrentJoint = defaultPose.trans() - basePose.trans(); + float ratio = glm::dot(baseToCurrentJoint, 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; +} + +void AnimSplineIK::beginInterp(InterpType interpType, const AnimChain& chain) { + // capture the current poses in a snapshot. + _snapshotChain = chain; + + _interpType = interpType; + _interpAlphaVel = FRAMES_PER_SECOND / _interpDuration; + _interpAlpha = 0.0f; +} diff --git a/libraries/animation/src/AnimSplineIK.h b/libraries/animation/src/AnimSplineIK.h new file mode 100644 index 0000000000..a4d8da37ca --- /dev/null +++ b/libraries/animation/src/AnimSplineIK.h @@ -0,0 +1,104 @@ +// +// AnimSplineIK.h +// +// Created by Angus Antley on 1/7/19. +// Copyright (c) 2019 High Fidelity, Inc. All rights reserved. +// +// 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_AnimSplineIK_h +#define hifi_AnimSplineIK_h + +#include "AnimNode.h" +#include "IKTarget.h" +#include "AnimChain.h" + +static const int MAX_NUMBER_FLEX_VARIABLES = 10; + +// Spline IK for the spine +class AnimSplineIK : public AnimNode { +public: + AnimSplineIK(const QString& id, float alpha, bool enabled, float interpDuration, + const QString& baseJointName, const QString& midJointName, const QString& tipJointName, + const QString& basePositionVar, const QString& baseRotationVar, + const QString& midPositionVar, const QString& midRotationVar, + const QString& tipPositionVar, const QString& tipRotationVar, + const QString& alphaVar, const QString& enabledVar, + const std::vector tipTargetFlexCoefficients, + const std::vector midTargetFlexCoefficients); + + virtual ~AnimSplineIK() override; + virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, const AnimContext& context, float dt, AnimVariantMap& triggersOut) override; + +protected: + + enum class InterpType { + None = 0, + SnapshotToUnderPoses, + SnapshotToSolve, + NumTypes + }; + + void computeAbsolutePoses(AnimPoseVec& absolutePoses) const; + void loadPoses(const AnimPoseVec& poses); + + // for AnimDebugDraw rendering + virtual const AnimPoseVec& getPosesInternal() const override; + virtual void setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) override; + + void lookUpIndices(); + void beginInterp(InterpType interpType, const AnimChain& chain); + + AnimPoseVec _poses; + + float _alpha; + bool _enabled; + float _interpDuration; + QString _baseJointName; + QString _midJointName; + QString _tipJointName; + QString _basePositionVar; + QString _baseRotationVar; + QString _midPositionVar; + QString _midRotationVar; + QString _tipPositionVar; + QString _tipRotationVar; + QString _alphaVar; // float - (0, 1) 0 means underPoses only, 1 means IK only. + QString _enabledVar; + + float _tipTargetFlexCoefficients[MAX_NUMBER_FLEX_VARIABLES]; + float _midTargetFlexCoefficients[MAX_NUMBER_FLEX_VARIABLES]; + int _numTipTargetFlexCoefficients { 0 }; + int _numMidTargetFlexCoefficients { 0 }; + + int _baseJointIndex { -1 }; + int _midJointIndex { -1 }; + int _tipJointIndex { -1 }; + + bool _previousEnableDebugIKTargets { false }; + + InterpType _interpType{ InterpType::None }; + float _interpAlphaVel{ 0.0f }; + float _interpAlpha{ 0.0f }; + AnimChain _snapshotChain; + + // used to pre-compute information about each joint influenced 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 solveTargetWithSpline(const AnimContext& context, int base, const IKTarget& target, const AnimPoseVec& absolutePoses, bool debug, AnimChain& chainInfoOut) const; + void computeAndCacheSplineJointInfosForIKTarget(const AnimContext& context, int base, const IKTarget& target) const; + const std::vector* findOrCreateSplineJointInfo(const AnimContext& context, int base, const IKTarget& target) const; + mutable std::map> _splineJointInfoMap; + + // no copies + AnimSplineIK(const AnimSplineIK&) = delete; + AnimSplineIK& operator=(const AnimSplineIK&) = delete; + +}; +#endif // hifi_AnimSplineIK_h diff --git a/libraries/animation/src/AnimStateMachine.cpp b/libraries/animation/src/AnimStateMachine.cpp index fb13b8e71c..2c5d4ad0f3 100644 --- a/libraries/animation/src/AnimStateMachine.cpp +++ b/libraries/animation/src/AnimStateMachine.cpp @@ -22,7 +22,6 @@ AnimStateMachine::~AnimStateMachine() { } const AnimPoseVec& AnimStateMachine::evaluate(const AnimVariantMap& animVars, const AnimContext& context, float dt, AnimVariantMap& triggersOut) { - float parentDebugAlpha = context.getDebugAlpha(_id); QString desiredStateID = animVars.lookup(_currentStateVar, _currentState->getID()); diff --git a/libraries/animation/src/AnimTwoBoneIK.cpp b/libraries/animation/src/AnimTwoBoneIK.cpp index 8960b15940..c91518d5db 100644 --- a/libraries/animation/src/AnimTwoBoneIK.cpp +++ b/libraries/animation/src/AnimTwoBoneIK.cpp @@ -156,7 +156,7 @@ const AnimPoseVec& AnimTwoBoneIK::evaluate(const AnimVariantMap& animVars, const glm::quat relMidRot = glm::angleAxis(midAngle, _midHingeAxis); // insert new relative pose into the chain and rebuild it. - ikChain.setRelativePoseAtJointIndex(_midJointIndex, AnimPose(relMidRot, underPoses[_midJointIndex].trans())); + ikChain.setRelativePoseAtJointIndex(_midJointIndex, AnimPose(underPoses[_midJointIndex].scale(), relMidRot, underPoses[_midJointIndex].trans())); ikChain.buildDirtyAbsolutePoses(); // recompute tip pose after mid joint has been rotated @@ -180,7 +180,7 @@ const AnimPoseVec& AnimTwoBoneIK::evaluate(const AnimVariantMap& animVars, const // transform result back into parent relative frame. glm::quat relBaseRot = glm::inverse(baseParentPose.rot()) * absRot; - ikChain.setRelativePoseAtJointIndex(_baseJointIndex, AnimPose(relBaseRot, underPoses[_baseJointIndex].trans())); + ikChain.setRelativePoseAtJointIndex(_baseJointIndex, AnimPose(underPoses[_baseJointIndex].scale(), relBaseRot, underPoses[_baseJointIndex].trans())); } // recompute midJoint pose after base has been rotated. @@ -189,7 +189,7 @@ const AnimPoseVec& AnimTwoBoneIK::evaluate(const AnimVariantMap& animVars, const // transform target rotation in to parent relative frame. glm::quat relTipRot = glm::inverse(midJointPose.rot()) * targetPose.rot(); - ikChain.setRelativePoseAtJointIndex(_tipJointIndex, AnimPose(relTipRot, underPoses[_tipJointIndex].trans())); + ikChain.setRelativePoseAtJointIndex(_tipJointIndex, AnimPose(underPoses[_tipJointIndex].scale(), relTipRot, underPoses[_tipJointIndex].trans())); // blend with the underChain ikChain.blend(underChain, alpha); diff --git a/libraries/animation/src/Flow.cpp b/libraries/animation/src/Flow.cpp new file mode 100644 index 0000000000..3bb80b9375 --- /dev/null +++ b/libraries/animation/src/Flow.cpp @@ -0,0 +1,783 @@ +// +// Flow.cpp +// +// Created by Luis Cuenca on 1/21/2019. +// Copyright 2019 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 "Flow.h" +#include "Rig.h" +#include "AnimSkeleton.h" + +const std::map PRESET_FLOW_DATA = { { "hair", FlowPhysicsSettings() }, +{ "skirt", FlowPhysicsSettings(true, 1.0f, DEFAULT_GRAVITY, 0.65f, 0.8f, 0.45f, 0.01f) }, +{ "breast", FlowPhysicsSettings(true, 1.0f, DEFAULT_GRAVITY, 0.65f, 0.8f, 0.45f, 0.01f) } }; + +const std::map PRESET_COLLISION_DATA = { + { "Spine2", FlowCollisionSettings(QUuid(), FlowCollisionType::CollisionSphere, glm::vec3(0.0f, 0.2f, 0.0f), 0.14f) }, + { "LeftArm", FlowCollisionSettings(QUuid(), FlowCollisionType::CollisionSphere, glm::vec3(0.0f, 0.02f, 0.0f), 0.05f) }, + { "RightArm", FlowCollisionSettings(QUuid(), FlowCollisionType::CollisionSphere, glm::vec3(0.0f, 0.02f, 0.0f), 0.05f) }, + { "HeadTop_End", FlowCollisionSettings(QUuid(), FlowCollisionType::CollisionSphere, glm::vec3(0.0f, -0.15f, 0.0f), 0.09f) } +}; + +FlowCollisionSphere::FlowCollisionSphere(const int& jointIndex, const FlowCollisionSettings& settings, bool isTouch) { + _jointIndex = jointIndex; + _radius = _initialRadius = settings._radius; + _offset = _initialOffset = settings._offset; + _entityID = settings._entityID; + _isTouch = isTouch; +} + +FlowCollisionResult FlowCollisionSphere::computeSphereCollision(const glm::vec3& point, float radius) const { + FlowCollisionResult result; + auto centerToJoint = point - _position; + result._distance = glm::length(centerToJoint) - radius; + result._offset = _radius - result._distance; + result._normal = glm::normalize(centerToJoint); + result._radius = _radius; + result._position = _position; + return result; +} + +FlowCollisionResult FlowCollisionSphere::checkSegmentCollision(const glm::vec3& point1, const glm::vec3& point2, const FlowCollisionResult& collisionResult1, const FlowCollisionResult& collisionResult2) { + FlowCollisionResult result; + auto segment = point2 - point1; + auto segmentLength = glm::length(segment); + auto maxDistance = glm::sqrt(powf(collisionResult1._radius, 2.0f) + powf(segmentLength, 2.0f)); + if (collisionResult1._distance < maxDistance && collisionResult2._distance < maxDistance) { + float segmentPercentage = collisionResult1._distance / (collisionResult1._distance + collisionResult2._distance); + glm::vec3 collisionPoint = point1 + segment * segmentPercentage; + glm::vec3 centerToSegment = collisionPoint - _position; + float distance = glm::length(centerToSegment); + if (distance < _radius) { + result._offset = _radius - distance; + result._position = _position; + result._radius = _radius; + result._normal = glm::normalize(centerToSegment); + result._distance = distance; + } + } + return result; +} + +void FlowCollisionSystem::addCollisionSphere(int jointIndex, const FlowCollisionSettings& settings, const glm::vec3& position, bool isSelfCollision, bool isTouch) { + auto collision = FlowCollisionSphere(jointIndex, settings, isTouch); + collision.setPosition(position); + if (isSelfCollision) { + _selfCollisions.push_back(collision); + } else { + _othersCollisions.push_back(collision); + } + +}; +void FlowCollisionSystem::resetCollisions() { + _allCollisions.clear(); + _othersCollisions.clear(); + _selfCollisions.clear(); +} +FlowCollisionResult FlowCollisionSystem::computeCollision(const std::vector collisions) { + FlowCollisionResult result; + if (collisions.size() > 1) { + for (size_t i = 0; i < collisions.size(); i++) { + result._offset += collisions[i]._offset; + result._normal = result._normal + collisions[i]._normal * collisions[i]._distance; + result._position = result._position + collisions[i]._position; + result._radius += collisions[i]._radius; + result._distance += collisions[i]._distance; + } + result._offset = result._offset / collisions.size(); + result._radius = 0.5f * glm::length(result._normal); + result._normal = glm::normalize(result._normal); + result._position = result._position / (float)collisions.size(); + result._distance = result._distance / collisions.size(); + } else if (collisions.size() == 1) { + result = collisions[0]; + } + result._count = (int)collisions.size(); + return result; +}; + +void FlowCollisionSystem::setScale(float scale) { + _scale = scale; + for (size_t j = 0; j < _selfCollisions.size(); j++) { + _selfCollisions[j]._radius = _selfCollisions[j]._initialRadius * scale; + _selfCollisions[j]._offset = _selfCollisions[j]._initialOffset * scale; + } +}; + +std::vector FlowCollisionSystem::checkFlowThreadCollisions(FlowThread* flowThread) { + std::vector> FlowThreadResults; + FlowThreadResults.resize(flowThread->_joints.size()); + for (size_t j = 0; j < _allCollisions.size(); j++) { + FlowCollisionSphere &sphere = _allCollisions[j]; + FlowCollisionResult rootCollision = sphere.computeSphereCollision(flowThread->_positions[0], flowThread->_radius); + std::vector collisionData = { rootCollision }; + bool tooFar = rootCollision._distance >(flowThread->_length + rootCollision._radius); + FlowCollisionResult nextCollision; + if (!tooFar) { + if (sphere._isTouch) { + for (size_t i = 1; i < flowThread->_joints.size(); i++) { + auto prevCollision = collisionData[i - 1]; + nextCollision = _allCollisions[j].computeSphereCollision(flowThread->_positions[i], flowThread->_radius); + collisionData.push_back(nextCollision); + if (prevCollision._offset > 0.0f) { + if (i == 1) { + FlowThreadResults[i - 1].push_back(prevCollision); + } + } else if (nextCollision._offset > 0.0f) { + FlowThreadResults[i].push_back(nextCollision); + } else { + FlowCollisionResult segmentCollision = _allCollisions[j].checkSegmentCollision(flowThread->_positions[i - 1], flowThread->_positions[i], prevCollision, nextCollision); + if (segmentCollision._offset > 0) { + FlowThreadResults[i - 1].push_back(segmentCollision); + FlowThreadResults[i].push_back(segmentCollision); + } + } + } + } else { + if (rootCollision._offset > 0.0f) { + FlowThreadResults[0].push_back(rootCollision); + } + for (size_t i = 1; i < flowThread->_joints.size(); i++) { + nextCollision = _allCollisions[j].computeSphereCollision(flowThread->_positions[i], flowThread->_radius); + if (nextCollision._offset > 0.0f) { + FlowThreadResults[i].push_back(nextCollision); + } + } + } + } + } + + std::vector results; + for (size_t i = 0; i < flowThread->_joints.size(); i++) { + results.push_back(computeCollision(FlowThreadResults[i])); + } + return results; +}; + +FlowCollisionSettings FlowCollisionSystem::getCollisionSettingsByJoint(int jointIndex) { + for (auto &collision : _selfCollisions) { + if (collision._jointIndex == jointIndex) { + return FlowCollisionSettings(collision._entityID, FlowCollisionType::CollisionSphere, collision._initialOffset, collision._initialRadius); + } + } + return FlowCollisionSettings(); +} +void FlowCollisionSystem::setCollisionSettingsByJoint(int jointIndex, const FlowCollisionSettings& settings) { + for (auto &collision : _selfCollisions) { + if (collision._jointIndex == jointIndex) { + collision._initialRadius = settings._radius; + collision._initialOffset = settings._offset; + collision._radius = _scale * settings._radius; + collision._offset = _scale * settings._offset; + } + } +} +void FlowCollisionSystem::prepareCollisions() { + _allCollisions.clear(); + _allCollisions.resize(_selfCollisions.size() + _othersCollisions.size()); + std::copy(_selfCollisions.begin(), _selfCollisions.begin() + _selfCollisions.size(), _allCollisions.begin()); + std::copy(_othersCollisions.begin(), _othersCollisions.begin() + _othersCollisions.size(), _allCollisions.begin() + _selfCollisions.size()); + _othersCollisions.clear(); +} + +FlowNode::FlowNode(const glm::vec3& initialPosition, FlowPhysicsSettings settings) { + _initialPosition = _previousPosition = _currentPosition = initialPosition; + _initialRadius = settings._radius; +} + +void FlowNode::update(float deltaTime, const glm::vec3& accelerationOffset) { + _acceleration = glm::vec3(0.0f, _settings._gravity, 0.0f); + _previousVelocity = _currentVelocity; + _currentVelocity = _currentPosition - _previousPosition; + _previousPosition = _currentPosition; + if (!_anchored) { + // Add inertia + const float FPS = 60.0f; + float timeRatio = _scale * (FPS * deltaTime); + float invertedTimeRatio = timeRatio > 0.0f ? 1.0f / timeRatio : 1.0f; + auto deltaVelocity = _previousVelocity - _currentVelocity; + auto centrifugeVector = glm::length(deltaVelocity) != 0.0f ? glm::normalize(deltaVelocity) : glm::vec3(); + _acceleration = _acceleration + centrifugeVector * _settings._inertia * glm::length(_currentVelocity) * invertedTimeRatio; + + // Add offset + _acceleration += accelerationOffset; + float accelerationFactor = powf(_settings._delta, 2.0f) * timeRatio; + glm::vec3 deltaAcceleration = _acceleration * accelerationFactor; + // Calculate new position + _currentPosition = _currentPosition + (_currentVelocity * _settings._damping) + deltaAcceleration; + } else { + _acceleration = glm::vec3(0.0f); + _currentVelocity = glm::vec3(0.0f); + } +}; + + +void FlowNode::solve(const glm::vec3& constrainPoint, float maxDistance, const FlowCollisionResult& collision) { + solveConstraints(constrainPoint, maxDistance); + solveCollisions(collision); +}; + +void FlowNode::solveConstraints(const glm::vec3& constrainPoint, float maxDistance) { + glm::vec3 constrainVector = _currentPosition - constrainPoint; + float difference = maxDistance / glm::length(constrainVector); + _currentPosition = difference < 1.0f ? constrainPoint + constrainVector * difference : _currentPosition; +}; + +void FlowNode::solveCollisions(const FlowCollisionResult& collision) { + _colliding = collision._offset > 0.0f; + _collision = collision; + if (_colliding) { + _currentPosition = _currentPosition + collision._normal * collision._offset; + _previousCollision = collision; + } else { + _previousCollision = FlowCollisionResult(); + } +}; + +FlowJoint::FlowJoint(int jointIndex, int parentIndex, int childIndex, const QString& name, const QString& group, const FlowPhysicsSettings& settings) { + _index = jointIndex; + _name = name; + _group = group; + _childIndex = childIndex; + _parentIndex = parentIndex; + FlowNode(glm::vec3(), settings); +}; + +void FlowJoint::setInitialData(const glm::vec3& initialPosition, const glm::vec3& initialTranslation, const glm::quat& initialRotation, const glm::vec3& parentPosition) { + _initialPosition = initialPosition; + _previousPosition = initialPosition; + _currentPosition = initialPosition; + _initialTranslation = initialTranslation; + _currentRotation = initialRotation; + _initialRotation = initialRotation; + _translationDirection = glm::normalize(_initialTranslation); + _parentPosition = parentPosition; + _initialLength = _length = glm::length(_initialPosition - parentPosition); +} + +void FlowJoint::setUpdatedData(const glm::vec3& updatedPosition, const glm::vec3& updatedTranslation, const glm::quat& updatedRotation, const glm::vec3& parentPosition, const glm::quat& parentWorldRotation) { + _updatedPosition = updatedPosition; + _updatedRotation = updatedRotation; + _updatedTranslation = updatedTranslation; + _parentPosition = parentPosition; + _parentWorldRotation = parentWorldRotation; +} + +void FlowJoint::setRecoveryPosition(const glm::vec3& recoveryPosition) { + _recoveryPosition = recoveryPosition; + _applyRecovery = true; +} + +void FlowJoint::update(float deltaTime) { + glm::vec3 accelerationOffset = glm::vec3(0.0f); + if (_settings._stiffness > 0.0f) { + glm::vec3 recoveryVector = _recoveryPosition - _currentPosition; + float recoveryFactor = powf(_settings._stiffness, 3.0f); + accelerationOffset = recoveryVector * recoveryFactor; + } + FlowNode::update(deltaTime, accelerationOffset); + if (_anchored) { + if (!_isHelper) { + _currentPosition = _updatedPosition; + } else { + _currentPosition = _parentPosition; + } + } +}; + +void FlowJoint::setScale(float scale, bool initScale) { + if (initScale) { + _initialLength = _length / scale; + } + _settings._radius = _initialRadius * scale; + _length = _initialLength * scale; + _scale = scale; +} + +void FlowJoint::solve(const FlowCollisionResult& collision) { + FlowNode::solve(_parentPosition, _length, collision); +}; + +void FlowJoint::toHelperJoint(const glm::vec3& initialPosition, float length) { + _initialPosition = initialPosition; + _isHelper = true; + _length = length; +} + +FlowThread::FlowThread(int rootIndex, std::map* joints) { + _jointsPointer = joints; + computeFlowThread(rootIndex); +} + +void FlowThread::resetLength() { + _length = 0.0f; + for (size_t i = 1; i < _joints.size(); i++) { + int index = _joints[i]; + _length += _jointsPointer->at(index)._length; + } +} + +void FlowThread::computeFlowThread(int rootIndex) { + int parentIndex = rootIndex; + if (_jointsPointer->size() == 0) { + return; + } + int childIndex = _jointsPointer->at(parentIndex)._childIndex; + std::vector indexes = { parentIndex }; + for (size_t i = 0; i < _jointsPointer->size(); i++) { + if (childIndex > -1) { + indexes.push_back(childIndex); + childIndex = _jointsPointer->at(childIndex)._childIndex; + } else { + break; + } + } + _length = 0.0f; + for (size_t i = 0; i < indexes.size(); i++) { + int index = indexes[i]; + _joints.push_back(index); + if (i > 0) { + _length += _jointsPointer->at(index)._length; + } + } +}; + +void FlowThread::computeRecovery() { + int parentIndex = _joints[0]; + auto parentJoint = _jointsPointer->at(parentIndex); + _jointsPointer->at(parentIndex)._recoveryPosition = parentJoint._recoveryPosition = parentJoint._currentPosition; + glm::quat parentRotation = parentJoint._parentWorldRotation * parentJoint._initialRotation; + for (size_t i = 1; i < _joints.size(); i++) { + auto joint = _jointsPointer->at(_joints[i]); + _jointsPointer->at(_joints[i])._recoveryPosition = joint._recoveryPosition = parentJoint._recoveryPosition + (parentRotation * (joint._initialTranslation * 0.01f)); + parentJoint = joint; + } +}; + +void FlowThread::update(float deltaTime) { + _positions.clear(); + auto &firstJoint = _jointsPointer->at(_joints[0]); + _radius = firstJoint._settings._radius; + computeRecovery(); + for (size_t i = 0; i < _joints.size(); i++) { + auto &joint = _jointsPointer->at(_joints[i]); + joint.update(deltaTime); + _positions.push_back(joint._currentPosition); + } +}; + +void FlowThread::solve(FlowCollisionSystem& collisionSystem) { + if (collisionSystem.getActive()) { + auto bodyCollisions = collisionSystem.checkFlowThreadCollisions(this); + for (size_t i = 0; i < _joints.size(); i++) { + int index = _joints[i]; + _jointsPointer->at(index).solve(bodyCollisions[i]); + } + } else { + for (size_t i = 0; i < _joints.size(); i++) { + int index = _joints[i]; + _jointsPointer->at(index).solve(FlowCollisionResult()); + } + } +}; + +void FlowThread::computeJointRotations() { + + auto pos0 = _rootFramePositions[0]; + auto pos1 = _rootFramePositions[1]; + + auto joint0 = _jointsPointer->at(_joints[0]); + auto joint1 = _jointsPointer->at(_joints[1]); + + auto initial_pos1 = pos0 + (joint0._initialRotation * (joint1._initialTranslation * 0.01f)); + + auto vec0 = initial_pos1 - pos0; + auto vec1 = pos1 - pos0; + + auto delta = rotationBetween(vec0, vec1); + + joint0._currentRotation = _jointsPointer->at(_joints[0])._currentRotation = delta * joint0._initialRotation; + + for (size_t i = 1; i < _joints.size() - 1; i++) { + auto nextJoint = _jointsPointer->at(_joints[i + 1]); + for (size_t j = i; j < _joints.size(); j++) { + _rootFramePositions[j] = glm::inverse(joint0._currentRotation) * _rootFramePositions[j] - (joint0._initialTranslation * 0.01f); + } + pos0 = _rootFramePositions[i]; + pos1 = _rootFramePositions[i + 1]; + initial_pos1 = pos0 + joint1._initialRotation * (nextJoint._initialTranslation * 0.01f); + + vec0 = initial_pos1 - pos0; + vec1 = pos1 - pos0; + + delta = rotationBetween(vec0, vec1); + + joint1._currentRotation = _jointsPointer->at(joint1._index)._currentRotation = delta * joint1._initialRotation; + joint0 = joint1; + joint1 = nextJoint; + } + +} + +void FlowThread::setScale(float scale, bool initScale) { + for (size_t i = 0; i < _joints.size(); i++) { + auto &joint = _jointsPointer->at(_joints[i]); + joint.setScale(scale, initScale); + } + resetLength(); +} + +FlowThread& FlowThread::operator=(const FlowThread& otherFlowThread) { + for (int jointIndex: otherFlowThread._joints) { + auto& joint = otherFlowThread._jointsPointer->at(jointIndex); + auto& myJoint = _jointsPointer->at(jointIndex); + myJoint._acceleration = joint._acceleration; + myJoint._currentPosition = joint._currentPosition; + myJoint._currentRotation = joint._currentRotation; + myJoint._currentVelocity = joint._currentVelocity; + myJoint._length = joint._length; + myJoint._parentPosition = joint._parentPosition; + myJoint._parentWorldRotation = joint._parentWorldRotation; + myJoint._previousPosition = joint._previousPosition; + myJoint._previousVelocity = joint._previousVelocity; + myJoint._scale = joint._scale; + myJoint._translationDirection = joint._translationDirection; + myJoint._updatedPosition = joint._updatedPosition; + myJoint._updatedRotation = joint._updatedRotation; + myJoint._updatedTranslation = joint._updatedTranslation; + myJoint._isHelper = joint._isHelper; + } + return *this; +} + +void Flow::calculateConstraints(const std::shared_ptr& skeleton, + AnimPoseVec& relativePoses, AnimPoseVec& absolutePoses) { + cleanUp(); + if (!skeleton) { + return; + } + auto flowPrefix = FLOW_JOINT_PREFIX.toUpper(); + auto simPrefix = SIM_JOINT_PREFIX.toUpper(); + std::vector handsIndices; + + for (int i = 0; i < skeleton->getNumJoints(); i++) { + auto name = skeleton->getJointName(i); + if (std::find(HAND_COLLISION_JOINTS.begin(), HAND_COLLISION_JOINTS.end(), name) != HAND_COLLISION_JOINTS.end()) { + handsIndices.push_back(i); + } + auto parentIndex = skeleton->getParentIndex(i); + if (parentIndex == -1) { + continue; + } + auto jointChildren = skeleton->getChildrenOfJoint(i); + // auto childIndex = jointChildren.size() > 0 ? jointChildren[0] : -1; + auto group = QStringRef(&name, 0, 3).toString().toUpper(); + auto split = name.split("_"); + bool isSimJoint = (group == simPrefix); + bool isFlowJoint = split.size() > 2 && split[0].toUpper() == flowPrefix; + if (isFlowJoint || isSimJoint) { + group = ""; + if (isSimJoint) { + for (int j = 1; j < name.size() - 1; j++) { + bool toFloatSuccess; + QStringRef(&name, (int)(name.size() - j), 1).toString().toFloat(&toFloatSuccess); + if (!toFloatSuccess && (name.size() - j) > (int)simPrefix.size()) { + group = QStringRef(&name, (int)simPrefix.size(), (int)(name.size() - j + 1)).toString(); + break; + } + } + if (group.isEmpty()) { + group = QStringRef(&name, (int)simPrefix.size(), name.size() - 1).toString(); + } + qCDebug(animation) << "Sim joint added to flow: " << name; + } else { + group = split[1]; + } + if (!group.isEmpty()) { + _flowJointKeywords.push_back(group); + FlowPhysicsSettings jointSettings; + if (PRESET_FLOW_DATA.find(group) != PRESET_FLOW_DATA.end()) { + jointSettings = PRESET_FLOW_DATA.at(group); + } else { + jointSettings = DEFAULT_JOINT_SETTINGS; + } + if (_flowJointData.find(i) == _flowJointData.end()) { + auto flowJoint = FlowJoint(i, parentIndex, -1, name, group, jointSettings); + _flowJointData.insert(std::pair(i, flowJoint)); + } + } + } else { + if (PRESET_COLLISION_DATA.find(name) != PRESET_COLLISION_DATA.end()) { + _collisionSystem.addCollisionSphere(i, PRESET_COLLISION_DATA.at(name)); + } + } + } + + for (auto &jointData : _flowJointData) { + int jointIndex = jointData.first; + glm::vec3 jointPosition, parentPosition, jointTranslation; + glm::quat jointRotation; + getJointPositionInWorldFrame(absolutePoses, jointIndex, jointPosition, _entityPosition, _entityRotation); + getJointTranslation(relativePoses, jointIndex, jointTranslation); + getJointRotation(relativePoses, jointIndex, jointRotation); + getJointPositionInWorldFrame(absolutePoses, jointData.second.getParentIndex(), parentPosition, _entityPosition, _entityRotation); + + jointData.second.setInitialData(jointPosition, jointTranslation, jointRotation, parentPosition); + } + + std::vector roots; + + for (auto &joint :_flowJointData) { + if (_flowJointData.find(joint.second.getParentIndex()) == _flowJointData.end()) { + joint.second.setAnchored(true); + roots.push_back(joint.first); + } else { + _flowJointData[joint.second.getParentIndex()].setChildIndex(joint.first); + } + } + int extraIndex = -1; + for (size_t i = 0; i < roots.size(); i++) { + FlowThread thread = FlowThread(roots[i], &_flowJointData); + // add threads with at least 2 joints + if (thread._joints.size() > 0) { + if (thread._joints.size() == 1) { + int jointIndex = roots[i]; + auto &joint = _flowJointData[jointIndex]; + auto &jointPosition = joint.getUpdatedPosition(); + auto newSettings = joint.getSettings(); + extraIndex = extraIndex > -1 ? extraIndex + 1 : skeleton->getNumJoints(); + joint.setChildIndex(extraIndex); + auto newJoint = FlowJoint(extraIndex, jointIndex, -1, joint.getName(), joint.getGroup(), newSettings); + newJoint.toHelperJoint(jointPosition, HELPER_JOINT_LENGTH); + glm::vec3 translation = glm::vec3(0.0f, HELPER_JOINT_LENGTH, 0.0f); + newJoint.setInitialData(jointPosition + translation, 100.0f * translation , Quaternions::IDENTITY, jointPosition); + _flowJointData.insert(std::pair(extraIndex, newJoint)); + FlowThread newThread = FlowThread(jointIndex, &_flowJointData); + if (newThread._joints.size() > 1) { + _jointThreads.push_back(newThread); + } + } else { + _jointThreads.push_back(thread); + } + } + } + + if (_jointThreads.size() == 0) { + onCleanup(); + } + if (handsIndices.size() > 0) { + FlowCollisionSettings handSettings; + handSettings._radius = HAND_COLLISION_RADIUS; + for (size_t i = 0; i < handsIndices.size(); i++) { + _collisionSystem.addCollisionSphere(handsIndices[i], handSettings, glm::vec3(), true, true); + } + } + _initialized = _jointThreads.size() > 0; +} + +void Flow::cleanUp() { + _flowJointData.clear(); + _jointThreads.clear(); + _flowJointKeywords.clear(); + _collisionSystem.resetCollisions(); + _initialized = false; + _isScaleSet = false; + onCleanup(); + } + +void Flow::setTransform(float scale, const glm::vec3& position, const glm::quat& rotation) { + _scale = scale; + _entityPosition = position; + _entityRotation = rotation; +} + +void Flow::setScale(float scale) { + _collisionSystem.setScale(_scale); + for (size_t i = 0; i < _jointThreads.size(); i++) { + _jointThreads[i].setScale(_scale, !_isScaleSet); + } + if (_lastScale != _scale) { + _lastScale = _scale; + _isScaleSet = true; + } + +} + +void Flow::update(float deltaTime, AnimPoseVec& relativePoses, AnimPoseVec& absolutePoses, const std::vector& overrideFlags) { + if (_initialized && _active) { + uint64_t startTime = usecTimestampNow(); + uint64_t updateExpiry = startTime + MAX_UPDATE_FLOW_TIME_BUDGET; + if (_scale != _lastScale) { + setScale(_scale); + } + for (size_t i = 0; i < _jointThreads.size(); i++) { + size_t index = _invertThreadLoop ? _jointThreads.size() - 1 - i : i; + auto &thread = _jointThreads[index]; + thread.update(deltaTime); + thread.solve(_collisionSystem); + if (!updateRootFramePositions(absolutePoses, index)) { + return; + } + thread.computeJointRotations(); + if (usecTimestampNow() > updateExpiry) { + break; + qWarning(animation) << "Flow Bones ran out of time while updating threads"; + } + } + setJoints(relativePoses, overrideFlags); + updateJoints(relativePoses, absolutePoses); + _invertThreadLoop = !_invertThreadLoop; + } +} + +void Flow::updateAbsolutePoses(const AnimPoseVec& relativePoses, AnimPoseVec& absolutePoses) { + for (auto &joint : _flowJointData) { + int index = joint.second.getIndex(); + int parentIndex = joint.second.getParentIndex(); + if (index >= 0 && index < (int)relativePoses.size() && + parentIndex >= 0 && parentIndex < (int)absolutePoses.size()) { + absolutePoses[index] = absolutePoses[parentIndex] * relativePoses[index]; + } + } +} + +bool Flow::worldToJointPoint(const AnimPoseVec& absolutePoses, const glm::vec3& position, const int jointIndex, glm::vec3& jointSpacePosition) const { + glm::vec3 jointPos; + glm::quat jointRot; + if (getJointPositionInWorldFrame(absolutePoses, jointIndex, jointPos, _entityPosition, _entityRotation) && + getJointRotationInWorldFrame(absolutePoses, jointIndex, jointRot, _entityRotation)) { + glm::vec3 modelOffset = position - jointPos; + jointSpacePosition = glm::inverse(jointRot) * modelOffset; + return true; + } + return false; +} + +bool Flow::updateRootFramePositions(const AnimPoseVec& absolutePoses, size_t threadIndex) { + auto &joints = _jointThreads[threadIndex]._joints; + int rootIndex = _flowJointData[joints[0]].getParentIndex(); + _jointThreads[threadIndex]._rootFramePositions.clear(); + for (size_t j = 0; j < joints.size(); j++) { + glm::vec3 jointPos; + if (worldToJointPoint(absolutePoses, _flowJointData[joints[j]].getCurrentPosition(), rootIndex, jointPos)) { + _jointThreads[threadIndex]._rootFramePositions.push_back(jointPos); + } else { + return false; + } + } + return true; +} + +void Flow::updateJoints(AnimPoseVec& relativePoses, AnimPoseVec& absolutePoses) { + updateAbsolutePoses(relativePoses, absolutePoses); + for (auto &jointData : _flowJointData) { + int jointIndex = jointData.first; + glm::vec3 jointPosition, parentPosition, jointTranslation; + glm::quat jointRotation, parentWorldRotation; + if (!jointData.second.isHelper()) { + getJointPositionInWorldFrame(absolutePoses, jointIndex, jointPosition, _entityPosition, _entityRotation); + getJointTranslation(relativePoses, jointIndex, jointTranslation); + getJointRotation(relativePoses, jointIndex, jointRotation); + } else { + jointPosition = jointData.second.getCurrentPosition(); + jointTranslation = jointData.second.getCurrentTranslation(); + jointRotation = jointData.second.getCurrentRotation(); + } + getJointPositionInWorldFrame(absolutePoses, jointData.second.getParentIndex(), parentPosition, _entityPosition, _entityRotation); + getJointRotationInWorldFrame(absolutePoses, jointData.second.getParentIndex(), parentWorldRotation, _entityRotation); + jointData.second.setUpdatedData(jointPosition, jointTranslation, jointRotation, parentPosition, parentWorldRotation); + } + auto &selfCollisions = _collisionSystem.getSelfCollisions(); + for (auto &collision : selfCollisions) { + glm::quat jointRotation; + getJointPositionInWorldFrame(absolutePoses, collision._jointIndex, collision._position, _entityPosition, _entityRotation); + getJointRotationInWorldFrame(absolutePoses, collision._jointIndex, jointRotation, _entityRotation); + glm::vec3 worldOffset = jointRotation * collision._offset; + collision._position = collision._position + worldOffset; + } + _collisionSystem.prepareCollisions(); +} + +void Flow::setJoints(AnimPoseVec& relativePoses, const std::vector& overrideFlags) { + for (auto &thread : _jointThreads) { + auto &joints = thread._joints; + for (int jointIndex : joints) { + auto &joint = _flowJointData[jointIndex]; + if (jointIndex >= 0 && jointIndex < (int)relativePoses.size() && !overrideFlags[jointIndex]) { + relativePoses[jointIndex].rot() = joint.getCurrentRotation(); + } + } + } +} + +void Flow::setOthersCollision(const QUuid& otherId, int jointIndex, const glm::vec3& position) { + FlowCollisionSettings settings; + settings._entityID = otherId; + settings._radius = HAND_COLLISION_RADIUS; + _collisionSystem.addCollisionSphere(jointIndex, settings, position, false, true); +} + +void Flow::setPhysicsSettingsForGroup(const QString& group, const FlowPhysicsSettings& settings) { + for (auto &joint : _flowJointData) { + if (joint.second.getGroup().toUpper() == group.toUpper()) { + joint.second.setSettings(settings); + } + } +} + +bool Flow::getJointPositionInWorldFrame(const AnimPoseVec& absolutePoses, int jointIndex, glm::vec3& position, glm::vec3 translation, glm::quat rotation) const { + if (jointIndex >= 0 && jointIndex < (int)absolutePoses.size()) { + glm::vec3 poseSetTrans = absolutePoses[jointIndex].trans(); + position = (rotation * poseSetTrans) + translation; + if (!isNaN(position)) { + return true; + } else { + position = glm::vec3(0.0f); + } + } + return false; +} + +bool Flow::getJointRotationInWorldFrame(const AnimPoseVec& absolutePoses, int jointIndex, glm::quat& result, const glm::quat& rotation) const { + if (jointIndex >= 0 && jointIndex < (int)absolutePoses.size()) { + result = rotation * absolutePoses[jointIndex].rot(); + return true; + } else { + return false; + } +} + +bool Flow::getJointRotation(const AnimPoseVec& relativePoses, int jointIndex, glm::quat& rotation) const { + if (jointIndex >= 0 && jointIndex < (int)relativePoses.size()) { + rotation = relativePoses[jointIndex].rot(); + return true; + } else { + return false; + } +} + +bool Flow::getJointTranslation(const AnimPoseVec& relativePoses, int jointIndex, glm::vec3& translation) const { + if (jointIndex >= 0 && jointIndex < (int)relativePoses.size()) { + translation = relativePoses[jointIndex].trans(); + return true; + } else { + return false; + } +} + +Flow& Flow::operator=(const Flow& otherFlow) { + _active = otherFlow.getActive(); + _scale = otherFlow.getScale(); + _isScaleSet = true; + auto &threads = otherFlow.getThreads(); + if (threads.size() == _jointThreads.size()) { + for (size_t i = 0; i < _jointThreads.size(); i++) { + _jointThreads[i] = threads[i]; + } + } + return *this; +} \ No newline at end of file diff --git a/libraries/animation/src/Flow.h b/libraries/animation/src/Flow.h new file mode 100644 index 0000000000..35464e9420 --- /dev/null +++ b/libraries/animation/src/Flow.h @@ -0,0 +1,328 @@ +// +// Flow.h +// +// Created by Luis Cuenca on 1/21/2019. +// Copyright 2019 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_Flow_h +#define hifi_Flow_h + +#include +#include +#include +#include +#include +#include +#include +#include "AnimPose.h" + +class Rig; +class AnimSkeleton; + +const float HAPTIC_TOUCH_STRENGTH = 0.25f; +const float HAPTIC_TOUCH_DURATION = 10.0f; +const float HAPTIC_SLOPE = 0.18f; +const float HAPTIC_THRESHOLD = 40.0f; + +const QString FLOW_JOINT_PREFIX = "flow"; +const QString SIM_JOINT_PREFIX = "sim"; + +const std::vector HAND_COLLISION_JOINTS = { "RightHandMiddle1", "RightHandThumb3", "LeftHandMiddle1", "LeftHandThumb3", "RightHandMiddle3", "LeftHandMiddle3" }; + +const float HAND_COLLISION_RADIUS = 0.03f; +const float HELPER_JOINT_LENGTH = 0.05f; + +const float DEFAULT_STIFFNESS = 0.0f; +const float DEFAULT_GRAVITY = -0.0096f; +const float DEFAULT_DAMPING = 0.85f; +const float DEFAULT_INERTIA = 0.8f; +const float DEFAULT_DELTA = 0.55f; +const float DEFAULT_RADIUS = 0.01f; + +const uint64_t MAX_UPDATE_FLOW_TIME_BUDGET = 2000; + +struct FlowPhysicsSettings { + FlowPhysicsSettings() {}; + FlowPhysicsSettings(bool active, float stiffness, float gravity, float damping, float inertia, float delta, float radius) { + _active = active; + _stiffness = stiffness; + _gravity = gravity; + _damping = damping; + _inertia = inertia; + _delta = delta; + _radius = radius; + } + bool _active{ true }; + float _stiffness{ DEFAULT_STIFFNESS }; + float _gravity{ DEFAULT_GRAVITY }; + float _damping{ DEFAULT_DAMPING }; + float _inertia{ DEFAULT_INERTIA }; + float _delta{ DEFAULT_DELTA }; + float _radius{ DEFAULT_RADIUS }; +}; + +enum FlowCollisionType { + CollisionSphere = 0 +}; + +struct FlowCollisionSettings { + FlowCollisionSettings() {}; + FlowCollisionSettings(const QUuid& id, const FlowCollisionType& type, const glm::vec3& offset, float radius) { + _entityID = id; + _type = type; + _offset = offset; + _radius = radius; + }; + QUuid _entityID; + FlowCollisionType _type { FlowCollisionType::CollisionSphere }; + float _radius { 0.05f }; + glm::vec3 _offset; +}; + +const FlowPhysicsSettings DEFAULT_JOINT_SETTINGS; + +struct FlowJointInfo { + FlowJointInfo() {}; + FlowJointInfo(int index, int parentIndex, int childIndex, const QString& name) { + _index = index; + _parentIndex = parentIndex; + _childIndex = childIndex; + _name = name; + } + int _index { -1 }; + QString _name; + int _parentIndex { -1 }; + int _childIndex { -1 }; +}; + +struct FlowCollisionResult { + int _count { 0 }; + float _offset { 0.0f }; + glm::vec3 _position; + float _radius { 0.0f }; + glm::vec3 _normal; + float _distance { 0.0f }; +}; + +class FlowCollisionSphere { +public: + FlowCollisionSphere() {}; + FlowCollisionSphere(const int& jointIndex, const FlowCollisionSettings& settings, bool isTouch = false); + void setPosition(const glm::vec3& position) { _position = position; } + FlowCollisionResult computeSphereCollision(const glm::vec3& point, float radius) const; + FlowCollisionResult checkSegmentCollision(const glm::vec3& point1, const glm::vec3& point2, const FlowCollisionResult& collisionResult1, const FlowCollisionResult& collisionResult2); + + QUuid _entityID; + + glm::vec3 _offset; + glm::vec3 _initialOffset; + glm::vec3 _position; + + bool _isTouch { false }; + int _jointIndex { -1 }; + int collisionIndex { -1 }; + float _radius { 0.0f }; + float _initialRadius{ 0.0f }; +}; + +class FlowThread; + +class FlowCollisionSystem { +public: + FlowCollisionSystem() {}; + void addCollisionSphere(int jointIndex, const FlowCollisionSettings& settings, const glm::vec3& position = { 0.0f, 0.0f, 0.0f }, bool isSelfCollision = true, bool isTouch = false); + FlowCollisionResult computeCollision(const std::vector collisions); + + std::vector checkFlowThreadCollisions(FlowThread* flowThread); + + std::vector& getSelfCollisions() { return _selfCollisions; }; + void setOthersCollisions(const std::vector& othersCollisions) { _othersCollisions = othersCollisions; } + void prepareCollisions(); + void resetCollisions(); + void resetOthersCollisions() { _othersCollisions.clear(); } + void setScale(float scale); + FlowCollisionSettings getCollisionSettingsByJoint(int jointIndex); + void setCollisionSettingsByJoint(int jointIndex, const FlowCollisionSettings& settings); + void setActive(bool active) { _active = active; } + bool getActive() const { return _active; } +protected: + std::vector _selfCollisions; + std::vector _othersCollisions; + std::vector _allCollisions; + float _scale { 1.0f }; + bool _active { false }; +}; + +class FlowNode { +public: + FlowNode() {}; + FlowNode(const glm::vec3& initialPosition, FlowPhysicsSettings settings); + + void update(float deltaTime, const glm::vec3& accelerationOffset); + void solve(const glm::vec3& constrainPoint, float maxDistance, const FlowCollisionResult& collision); + void solveConstraints(const glm::vec3& constrainPoint, float maxDistance); + void solveCollisions(const FlowCollisionResult& collision); + +protected: + + FlowPhysicsSettings _settings; + glm::vec3 _initialPosition; + glm::vec3 _previousPosition; + glm::vec3 _currentPosition; + + glm::vec3 _currentVelocity; + glm::vec3 _previousVelocity; + glm::vec3 _acceleration; + + FlowCollisionResult _collision; + FlowCollisionResult _previousCollision; + + float _initialRadius { 0.0f }; + + bool _anchored { false }; + bool _colliding { false }; + bool _active { true }; + + float _scale{ 1.0f }; +}; + +class FlowJoint : public FlowNode { +public: + friend class FlowThread; + + FlowJoint(): FlowNode() {}; + FlowJoint(int jointIndex, int parentIndex, int childIndex, const QString& name, const QString& group, const FlowPhysicsSettings& settings); + void toHelperJoint(const glm::vec3& initialPosition, float length); + void setInitialData(const glm::vec3& initialPosition, const glm::vec3& initialTranslation, const glm::quat& initialRotation, const glm::vec3& parentPosition); + void setUpdatedData(const glm::vec3& updatedPosition, const glm::vec3& updatedTranslation, const glm::quat& updatedRotation, const glm::vec3& parentPosition, const glm::quat& parentWorldRotation); + void setRecoveryPosition(const glm::vec3& recoveryPosition); + void update(float deltaTime); + void solve(const FlowCollisionResult& collision); + + void setScale(float scale, bool initScale); + bool isAnchored() const { return _anchored; } + void setAnchored(bool anchored) { _anchored = anchored; } + bool isHelper() const { return _isHelper; } + + const FlowPhysicsSettings& getSettings() { return _settings; } + void setSettings(const FlowPhysicsSettings& settings) { _settings = settings; } + + const glm::vec3& getCurrentPosition() const { return _currentPosition; } + int getIndex() const { return _index; } + int getParentIndex() const { return _parentIndex; } + void setChildIndex(int index) { _childIndex = index; } + const glm::vec3& getUpdatedPosition() const { return _updatedPosition; } + const QString& getGroup() const { return _group; } + const QString& getName() const { return _name; } + const glm::quat& getCurrentRotation() const { return _currentRotation; } + const glm::vec3& getCurrentTranslation() const { return _initialTranslation; } + const glm::vec3& getInitialPosition() const { return _initialPosition; } + +protected: + + int _index{ -1 }; + int _parentIndex{ -1 }; + int _childIndex{ -1 }; + QString _name; + QString _group; + + bool _isHelper{ false }; + + glm::vec3 _initialTranslation; + glm::quat _initialRotation; + + glm::vec3 _updatedPosition; + glm::vec3 _updatedTranslation; + glm::quat _updatedRotation; + + glm::quat _currentRotation; + glm::vec3 _recoveryPosition; + + glm::vec3 _parentPosition; + glm::quat _parentWorldRotation; + glm::vec3 _translationDirection; + + float _length { 0.0f }; + float _initialLength { 0.0f }; + + bool _applyRecovery { false }; +}; + +class FlowThread { +public: + FlowThread() {}; + FlowThread& operator=(const FlowThread& otherFlowThread); + + FlowThread(int rootIndex, std::map* joints); + + void resetLength(); + void computeFlowThread(int rootIndex); + void computeRecovery(); + void update(float deltaTime); + void solve(FlowCollisionSystem& collisionSystem); + void computeJointRotations(); + void setRootFramePositions(const std::vector& rootFramePositions) { _rootFramePositions = rootFramePositions; } + void setScale(float scale, bool initScale = false); + + std::vector _joints; + std::vector _positions; + float _radius{ 0.0f }; + float _length{ 0.0f }; + std::map* _jointsPointer; + std::vector _rootFramePositions; +}; + +class Flow : public QObject{ + Q_OBJECT +public: + Flow() { } + Flow& operator=(const Flow& otherFlow); + bool getActive() const { return _active; } + void setActive(bool active) { _active = active; } + bool isInitialized() const { return _initialized; } + float getScale() const { return _scale; } + void calculateConstraints(const std::shared_ptr& skeleton, AnimPoseVec& relativePoses, AnimPoseVec& absolutePoses); + void update(float deltaTime, AnimPoseVec& relativePoses, AnimPoseVec& absolutePoses, const std::vector& overrideFlags); + void setTransform(float scale, const glm::vec3& position, const glm::quat& rotation); + const std::map& getJoints() const { return _flowJointData; } + const std::vector& getThreads() const { return _jointThreads; } + void setOthersCollision(const QUuid& otherId, int jointIndex, const glm::vec3& position); + FlowCollisionSystem& getCollisionSystem() { return _collisionSystem; } + void setPhysicsSettingsForGroup(const QString& group, const FlowPhysicsSettings& settings); + void cleanUp(); + +signals: + void onCleanup(); + +private: + void updateAbsolutePoses(const AnimPoseVec& relativePoses, AnimPoseVec& absolutePoses); + bool getJointPositionInWorldFrame(const AnimPoseVec& absolutePoses, int jointIndex, glm::vec3& position, glm::vec3 translation, glm::quat rotation) const; + bool getJointRotationInWorldFrame(const AnimPoseVec& absolutePoses, int jointIndex, glm::quat& result, const glm::quat& rotation) const; + bool getJointRotation(const AnimPoseVec& relativePoses, int jointIndex, glm::quat& rotation) const; + bool getJointTranslation(const AnimPoseVec& relativePoses, int jointIndex, glm::vec3& translation) const; + bool worldToJointPoint(const AnimPoseVec& absolutePoses, const glm::vec3& position, const int jointIndex, glm::vec3& jointSpacePosition) const; + + void setJoints(AnimPoseVec& relativePoses, const std::vector& overrideFlags); + void updateJoints(AnimPoseVec& relativePoses, AnimPoseVec& absolutePoses); + bool updateRootFramePositions(const AnimPoseVec& absolutePoses, size_t threadIndex); + void setScale(float scale); + + float _scale { 1.0f }; + float _lastScale{ 1.0f }; + glm::vec3 _entityPosition; + glm::quat _entityRotation; + std::map _flowJointData; + std::vector _jointThreads; + std::vector _flowJointKeywords; + FlowCollisionSystem _collisionSystem; + bool _initialized { false }; + bool _active { false }; + bool _isScaleSet { false }; + bool _invertThreadLoop { false }; +}; + +#endif // hifi_Flow_h diff --git a/libraries/animation/src/IKTarget.h b/libraries/animation/src/IKTarget.h index 7e53e6a7ea..564dba7f05 100644 --- a/libraries/animation/src/IKTarget.h +++ b/libraries/animation/src/IKTarget.h @@ -56,6 +56,8 @@ public: bool getPoleVectorEnabled() const { return _poleVectorEnabled; } int getIndex() const { return _index; } Type getType() const { return _type; } + int getNumFlexCoefficients() const { return (int)_numFlexCoefficients; } + float getFlexCoefficient(size_t chainDepth) const; void setPose(const glm::quat& rotation, const glm::vec3& translation); void setPoleVector(const glm::vec3& poleVector) { _poleVector = poleVector; } @@ -64,7 +66,6 @@ public: void setIndex(int index) { _index = index; } void setType(int); void setFlexCoefficients(size_t numFlexCoefficientsIn, const float* flexCoefficientsIn); - float getFlexCoefficient(size_t chainDepth) const; void setWeight(float weight) { _weight = weight; } float getWeight() const { return _weight; } diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 1ab680fba2..2a4c2326db 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -34,7 +34,6 @@ #include "IKTarget.h" #include "PathUtils.h" - static int nextRigId = 1; static std::map rigRegistry; static std::mutex rigRegistryMutex; @@ -74,6 +73,20 @@ static const QString RIGHT_FOOT_IK_ROTATION_VAR("rightFootIKRotationVar"); static const QString MAIN_STATE_MACHINE_RIGHT_FOOT_ROTATION("mainStateMachineRightFootRotation"); static const QString MAIN_STATE_MACHINE_RIGHT_FOOT_POSITION("mainStateMachineRightFootPosition"); +static const QString LEFT_HAND_POSITION("leftHandPosition"); +static const QString LEFT_HAND_ROTATION("leftHandRotation"); +static const QString LEFT_HAND_IK_POSITION_VAR("leftHandIKPositionVar"); +static const QString LEFT_HAND_IK_ROTATION_VAR("leftHandIKRotationVar"); +static const QString MAIN_STATE_MACHINE_LEFT_HAND_POSITION("mainStateMachineLeftHandPosition"); +static const QString MAIN_STATE_MACHINE_LEFT_HAND_ROTATION("mainStateMachineLeftHandRotation"); + +static const QString RIGHT_HAND_POSITION("rightHandPosition"); +static const QString RIGHT_HAND_ROTATION("rightHandRotation"); +static const QString RIGHT_HAND_IK_POSITION_VAR("rightHandIKPositionVar"); +static const QString RIGHT_HAND_IK_ROTATION_VAR("rightHandIKRotationVar"); +static const QString MAIN_STATE_MACHINE_RIGHT_HAND_ROTATION("mainStateMachineRightHandRotation"); +static const QString MAIN_STATE_MACHINE_RIGHT_HAND_POSITION("mainStateMachineRightHandPosition"); + /**jsdoc *

An AnimStateDictionary object may have the following properties. It may also have other properties, set by @@ -573,7 +586,6 @@ void Rig::reset(const HFMModel& hfmModel) { _animSkeleton = std::make_shared(hfmModel); - _internalPoseSet._relativePoses.clear(); _internalPoseSet._relativePoses = _animSkeleton->getRelativeDefaultPoses(); @@ -957,7 +969,8 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos glm::vec3 forward = worldRotation * IDENTITY_FORWARD; glm::vec3 workingVelocity = worldVelocity; - + _internalFlow.setTransform(sensorToWorldScale, worldPosition, worldRotation * Quaternions::Y_180); + _networkFlow.setTransform(sensorToWorldScale, worldPosition, worldRotation * Quaternions::Y_180); { glm::vec3 localVel = glm::inverse(worldRotation) * workingVelocity; @@ -1262,16 +1275,23 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos t += deltaTime; - if (_enableInverseKinematics != _lastEnableInverseKinematics) { - if (_enableInverseKinematics) { - _animVars.set("ikOverlayAlpha", 1.0f); - } else { - _animVars.set("ikOverlayAlpha", 0.0f); - } + if (_enableInverseKinematics) { + _animVars.set("ikOverlayAlpha", 1.0f); + } else { + _animVars.set("ikOverlayAlpha", 0.0f); + _animVars.set("splineIKEnabled", false); + _animVars.set("leftHandIKEnabled", false); + _animVars.set("rightHandIKEnabled", false); + _animVars.set("leftFootIKEnabled", false); + _animVars.set("rightFootIKEnabled", false); + _animVars.set("leftHandPoleVectorEnabled", false); + _animVars.set("rightHandPoleVectorEnabled", false); + _animVars.set("leftFootPoleVectorEnabled", false); + _animVars.set("rightFootPoleVectorEnabled", false); } _lastEnableInverseKinematics = _enableInverseKinematics; - } + } _lastForward = forward; _lastPosition = worldPosition; _lastVelocity = workingVelocity; @@ -1419,12 +1439,26 @@ void Rig::updateAnimations(float deltaTime, const glm::mat4& rootTransform, cons _networkVars = networkTriggersOut; _lastContext = context; } + applyOverridePoses(); - buildAbsoluteRigPoses(_internalPoseSet._relativePoses, _internalPoseSet._absolutePoses); - buildAbsoluteRigPoses(_networkPoseSet._relativePoses, _networkPoseSet._absolutePoses); + + buildAbsoluteRigPoses(_internalPoseSet._relativePoses, _internalPoseSet._absolutePoses); + _internalFlow.update(deltaTime, _internalPoseSet._relativePoses, _internalPoseSet._absolutePoses, _internalPoseSet._overrideFlags); + + if (_sendNetworkNode) { + if (_internalFlow.getActive() && !_networkFlow.getActive()) { + _networkFlow = _internalFlow; + } + buildAbsoluteRigPoses(_networkPoseSet._relativePoses, _networkPoseSet._absolutePoses); + _networkFlow.update(deltaTime, _networkPoseSet._relativePoses, _networkPoseSet._absolutePoses, _internalPoseSet._overrideFlags); + } else if (_networkFlow.getActive()) { + _networkFlow.setActive(false); + } + // copy internal poses to external poses { QWriteLocker writeLock(&_externalPoseSetLock); + _externalPoseSet = _internalPoseSet; } } @@ -1462,6 +1496,7 @@ void Rig::computeHeadFromHMD(const AnimPose& hmdPose, glm::vec3& headPositionOut void Rig::updateHead(bool headEnabled, bool hipsEnabled, const AnimPose& headPose) { if (_animSkeleton) { if (headEnabled) { + _animVars.set("splineIKEnabled", true); _animVars.set("headPosition", headPose.trans()); _animVars.set("headRotation", headPose.rot()); if (hipsEnabled) { @@ -1476,6 +1511,7 @@ void Rig::updateHead(bool headEnabled, bool hipsEnabled, const AnimPose& headPos _animVars.set("headWeight", 8.0f); } } else { + _animVars.set("splineIKEnabled", false); _animVars.unset("headPosition"); _animVars.set("headRotation", headPose.rot()); _animVars.set("headType", (int)IKTarget::Type::RotationOnly); @@ -1607,8 +1643,22 @@ void Rig::updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnab const bool ENABLE_POLE_VECTORS = true; + if (headEnabled) { + // always do IK if head is enabled + _animVars.set("leftHandIKEnabled", true); + _animVars.set("rightHandIKEnabled", true); + } else { + // only do IK if we have a valid foot. + _animVars.set("leftHandIKEnabled", leftHandEnabled); + _animVars.set("rightHandIKEnabled", rightHandEnabled); + } + if (leftHandEnabled) { + // we need this for twoBoneIK version of hands. + _animVars.set(LEFT_HAND_IK_POSITION_VAR, LEFT_HAND_POSITION); + _animVars.set(LEFT_HAND_IK_ROTATION_VAR, LEFT_HAND_ROTATION); + glm::vec3 handPosition = leftHandPose.trans(); glm::quat handRotation = leftHandPose.rot(); @@ -1641,8 +1691,11 @@ void Rig::updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnab _animVars.set("leftHandPoleVectorEnabled", false); } } else { - _animVars.set("leftHandPoleVectorEnabled", false); + // need this for two bone ik + _animVars.set(LEFT_HAND_IK_POSITION_VAR, MAIN_STATE_MACHINE_LEFT_HAND_POSITION); + _animVars.set(LEFT_HAND_IK_ROTATION_VAR, MAIN_STATE_MACHINE_LEFT_HAND_ROTATION); + _animVars.set("leftHandPoleVectorEnabled", false); _animVars.unset("leftHandPosition"); _animVars.unset("leftHandRotation"); @@ -1656,6 +1709,10 @@ void Rig::updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnab if (rightHandEnabled) { + // need this for two bone IK + _animVars.set(RIGHT_HAND_IK_POSITION_VAR, RIGHT_HAND_POSITION); + _animVars.set(RIGHT_HAND_IK_ROTATION_VAR, RIGHT_HAND_ROTATION); + glm::vec3 handPosition = rightHandPose.trans(); glm::quat handRotation = rightHandPose.rot(); @@ -1689,8 +1746,12 @@ void Rig::updateHands(bool leftHandEnabled, bool rightHandEnabled, bool hipsEnab _animVars.set("rightHandPoleVectorEnabled", false); } } else { - _animVars.set("rightHandPoleVectorEnabled", false); + // need this for two bone IK + _animVars.set(RIGHT_HAND_IK_POSITION_VAR, MAIN_STATE_MACHINE_RIGHT_HAND_POSITION); + _animVars.set(RIGHT_HAND_IK_ROTATION_VAR, MAIN_STATE_MACHINE_RIGHT_HAND_ROTATION); + + _animVars.set("rightHandPoleVectorEnabled", false); _animVars.unset("rightHandPosition"); _animVars.unset("rightHandRotation"); @@ -1908,6 +1969,7 @@ bool Rig::calculateElbowPoleVector(int handIndex, int elbowIndex, int armIndex, correctionVector = forwardAmount * frontVector; } poleVector = glm::normalize(attenuationVector + fullPoleVector + correctionVector); + return true; } @@ -2030,7 +2092,7 @@ void Rig::updateFromControllerParameters(const ControllerParameters& params, flo std::shared_ptr ikNode = getAnimInverseKinematicsNode(); for (int i = 0; i < (int)NumSecondaryControllerTypes; i++) { int index = indexOfJoint(secondaryControllerJointNames[i]); - if (index >= 0) { + if ((index >= 0) && (ikNode)) { if (params.secondaryControllerFlags[i] & (uint8_t)ControllerFlags::Enabled) { ikNode->setSecondaryTargetInRigFrame(index, params.secondaryControllerPoses[i]); } else { @@ -2077,7 +2139,6 @@ void Rig::initAnimGraph(const QUrl& url) { auto roleState = roleAnimState.second; overrideRoleAnimation(roleState.role, roleState.url, roleState.fps, roleState.loop, roleState.firstFrame, roleState.lastFrame); } - emit onLoadComplete(); }); connect(_animLoader.get(), &AnimNodeLoader::error, [url](int error, QString str) { @@ -2316,3 +2377,16 @@ void Rig::computeAvatarBoundingCapsule( glm::vec3 capsuleCenter = transformPoint(_geometryToRigTransform, (0.5f * (totalExtents.maximum + totalExtents.minimum))); localOffsetOut = capsuleCenter - hipsPosition; } + +void Rig::initFlow(bool isActive) { + _internalFlow.setActive(isActive); + if (isActive) { + if (!_internalFlow.isInitialized()) { + _internalFlow.calculateConstraints(_animSkeleton, _internalPoseSet._relativePoses, _internalPoseSet._absolutePoses); + _networkFlow.calculateConstraints(_animSkeleton, _internalPoseSet._relativePoses, _internalPoseSet._absolutePoses); + } + } else { + _internalFlow.cleanUp(); + _networkFlow.cleanUp(); + } +} diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 41c25a3c3e..2f0e2ad65b 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -25,6 +25,7 @@ #include "AnimNodeLoader.h" #include "SimpleMovingAverage.h" #include "AnimUtil.h" +#include "Flow.h" class Rig; class AnimInverseKinematics; @@ -233,6 +234,9 @@ public: const AnimContext::DebugAlphaMap& getDebugAlphaMap() const { return _lastContext.getDebugAlphaMap(); } const AnimVariantMap& getAnimVars() const { return _lastAnimVars; } const AnimContext::DebugStateMachineMap& getStateMachineMap() const { return _lastContext.getStateMachineMap(); } + void initFlow(bool isActive); + Flow& getFlow() { return _internalFlow; } + signals: void onLoadComplete(); @@ -424,6 +428,8 @@ protected: SnapshotBlendPoseHelper _hipsBlendHelper; ControllerParameters _previousControllerParameters; + Flow _internalFlow; + Flow _networkFlow; }; #endif /* defined(__hifi__Rig__) */ diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 60a95ff58a..8c50a195ee 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -175,7 +175,7 @@ static float computeLoudness(int16_t* samples, int numSamples, int numChannels, const int32_t CLIPPING_THRESHOLD = 32392; // -0.1 dBFS const int32_t CLIPPING_DETECTION = 3; // consecutive samples over threshold - float scale = numSamples ? 1.0f / (numSamples * 32768.0f) : 0.0f; + float scale = numSamples ? 1.0f / numSamples : 0.0f; int32_t loudness = 0; isClipping = false; @@ -249,6 +249,8 @@ AudioClient::AudioClient() : _outputBufferSizeFrames("audioOutputBufferFrames", DEFAULT_BUFFER_FRAMES), _sessionOutputBufferSizeFrames(_outputBufferSizeFrames.get()), _outputStarveDetectionEnabled("audioOutputStarveDetectionEnabled", DEFAULT_STARVE_DETECTION_ENABLED), + _lastRawInputLoudness(0.0f), + _lastSmoothedRawInputLoudness(0.0f), _lastInputLoudness(0.0f), _timeSinceLastClip(-1.0f), _muted(false), @@ -1144,6 +1146,9 @@ void AudioClient::handleAudioInput(QByteArray& audioBuffer) { emit inputReceived(audioBuffer); } + // loudness after mute/gate + _lastInputLoudness = (_muted || !audioGateOpen) ? 0.0f : _lastRawInputLoudness; + // detect gate opening and closing bool openedInLastBlock = !_audioGateOpen && audioGateOpen; // the gate just opened bool closedInLastBlock = _audioGateOpen && !audioGateOpen; // the gate just closed @@ -1222,12 +1227,15 @@ void AudioClient::handleMicAudioInput() { // detect loudness and clipping on the raw input bool isClipping = false; - float inputLoudness = computeLoudness(inputAudioSamples.get(), inputSamplesRequired, _inputFormat.channelCount(), isClipping); + float loudness = computeLoudness(inputAudioSamples.get(), inputSamplesRequired, _inputFormat.channelCount(), isClipping); + _lastRawInputLoudness = loudness; - float tc = (inputLoudness > _lastInputLoudness) ? 0.378f : 0.967f; // 10ms attack, 300ms release @ 100Hz - inputLoudness += tc * (_lastInputLoudness - inputLoudness); - _lastInputLoudness = inputLoudness; + // envelope detection + float tc = (loudness > _lastSmoothedRawInputLoudness) ? 0.378f : 0.967f; // 10ms attack, 300ms release @ 100Hz + loudness += tc * (_lastSmoothedRawInputLoudness - loudness); + _lastSmoothedRawInputLoudness = loudness; + // clipping indicator if (isClipping) { _timeSinceLastClip = 0.0f; } else if (_timeSinceLastClip >= 0.0f) { @@ -1235,7 +1243,7 @@ void AudioClient::handleMicAudioInput() { } isClipping = (_timeSinceLastClip >= 0.0f) && (_timeSinceLastClip < 2.0f); // 2 second hold time - emit inputLoudnessChanged(_lastInputLoudness, isClipping); + emit inputLoudnessChanged(_lastSmoothedRawInputLoudness, isClipping); if (!_muted) { possibleResampling(_inputToNetworkResampler, diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 94ed2ce132..29036b7c71 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -127,7 +127,7 @@ public: const QAudioFormat& getOutputFormat() const { return _outputFormat; } - float getLastInputLoudness() const { return _lastInputLoudness; } // TODO: relative to noise floor? + float getLastInputLoudness() const { return _lastInputLoudness; } float getTimeSinceLastClip() const { return _timeSinceLastClip; } float getAudioAverageInputLoudness() const { return _lastInputLoudness; } @@ -355,7 +355,9 @@ private: StDev _stdev; QElapsedTimer _timeSinceLastReceived; - float _lastInputLoudness; + float _lastRawInputLoudness; // before mute/gate + float _lastSmoothedRawInputLoudness; + float _lastInputLoudness; // after mute/gate float _timeSinceLastClip; int _totalInputAudioSamples; diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 7d0fc3409a..d3ae030296 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -37,6 +37,7 @@ #include "RenderableModelEntityItem.h" #include +#include #include "Logging.h" @@ -64,7 +65,11 @@ namespace render { return keyBuilder.build(); } template <> const Item::Bound payloadGetBound(const AvatarSharedPointer& avatar) { - return static_pointer_cast(avatar)->getRenderBounds(); + auto avatarPtr = static_pointer_cast(avatar); + if (avatarPtr) { + return avatarPtr->getRenderBounds(); + } + return Item::Bound(); } template <> void payloadRender(const AvatarSharedPointer& avatar, RenderArgs* args) { auto avatarPtr = static_pointer_cast(avatar); @@ -75,10 +80,15 @@ namespace render { } template <> uint32_t metaFetchMetaSubItems(const AvatarSharedPointer& avatar, ItemIDs& subItems) { auto avatarPtr = static_pointer_cast(avatar); - if (avatarPtr->getSkeletonModel()) { - auto& metaSubItems = avatarPtr->getSkeletonModel()->fetchRenderItemIDs(); - subItems.insert(subItems.end(), metaSubItems.begin(), metaSubItems.end()); - return (uint32_t) metaSubItems.size(); + if (avatarPtr) { + uint32_t total = 0; + if (avatarPtr->getSkeletonModel()) { + auto& metaSubItems = avatarPtr->getSkeletonModel()->fetchRenderItemIDs(); + subItems.insert(subItems.end(), metaSubItems.begin(), metaSubItems.end()); + total += (uint32_t)metaSubItems.size(); + } + total += avatarPtr->appendSubMetaItems(subItems); + return total; } return 0; } @@ -626,12 +636,18 @@ void Avatar::addToScene(AvatarSharedPointer self, const render::ScenePointer& sc _skeletonModel->setVisibleInScene(_isMeshVisible, scene); processMaterials(); + bool attachmentRenderingNeedsUpdate = false; for (auto& attachmentModel : _attachmentModels) { attachmentModel->addToScene(scene, transaction); attachmentModel->setTagMask(render::hifi::TAG_ALL_VIEWS); - attachmentModel->setGroupCulled(false); + attachmentModel->setGroupCulled(true); attachmentModel->setCanCastShadow(true); attachmentModel->setVisibleInScene(_isMeshVisible, scene); + attachmentRenderingNeedsUpdate = true; + } + + if (attachmentRenderingNeedsUpdate) { + updateAttachmentRenderIDs(); } _mustFadeIn = true; @@ -855,15 +871,17 @@ void Avatar::fixupModelsInScene(const render::ScenePointer& scene) { canTryFade = true; _isAnimatingScale = true; } + bool attachmentRenderingNeedsUpdate = false; for (auto attachmentModel : _attachmentModels) { if (attachmentModel->isRenderable() && attachmentModel->needsFixupInScene()) { attachmentModel->removeFromScene(scene, transaction); attachmentModel->addToScene(scene, transaction); attachmentModel->setTagMask(render::hifi::TAG_ALL_VIEWS); - attachmentModel->setGroupCulled(false); + attachmentModel->setGroupCulled(true); attachmentModel->setCanCastShadow(true); attachmentModel->setVisibleInScene(_isMeshVisible, scene); + attachmentRenderingNeedsUpdate = true; } } @@ -886,9 +904,15 @@ void Avatar::fixupModelsInScene(const render::ScenePointer& scene) { for (auto attachmentModelToRemove : _attachmentsToRemove) { attachmentModelToRemove->removeFromScene(scene, transaction); + attachmentRenderingNeedsUpdate = true; } _attachmentsToDelete.insert(_attachmentsToDelete.end(), _attachmentsToRemove.begin(), _attachmentsToRemove.end()); _attachmentsToRemove.clear(); + + if (attachmentRenderingNeedsUpdate) { + updateAttachmentRenderIDs(); + } + scene->enqueueTransaction(transaction); } @@ -931,6 +955,11 @@ void Avatar::simulateAttachments(float deltaTime) { } } } + + if (_ancestorChainRenderableVersion != _lastAncestorChainRenderableVersion) { + _lastAncestorChainRenderableVersion = _ancestorChainRenderableVersion; + updateDescendantRenderIDs(); + } } float Avatar::getBoundingRadius() const { @@ -1506,12 +1535,15 @@ void Avatar::setModelURLFinished(bool success) { // rig is ready void Avatar::rigReady() { buildUnscaledEyeHeightCache(); + buildSpine2SplineRatioCache(); computeMultiSphereShapes(); + buildSpine2SplineRatioCache(); } // rig has been reset. void Avatar::rigReset() { clearUnscaledEyeHeightCache(); + clearSpine2SplineRatioCache(); } void Avatar::computeMultiSphereShapes() { @@ -1608,7 +1640,6 @@ void Avatar::setAttachmentData(const QVector& attachmentData) { } } - int Avatar::parseDataFromBuffer(const QByteArray& buffer) { PerformanceTimer perfTimer("unpack"); if (!_initialized) { @@ -1967,10 +1998,43 @@ void Avatar::buildUnscaledEyeHeightCache() { } } +void Avatar::buildSpine2SplineRatioCache() { + if (_skeletonModel) { + auto& rig = _skeletonModel->getRig(); + AnimPose hipsRigDefaultPose = rig.getAbsoluteDefaultPose(rig.indexOfJoint("Hips")); + AnimPose headRigDefaultPose(rig.getAbsoluteDefaultPose(rig.indexOfJoint("Head"))); + glm::vec3 basePosition = hipsRigDefaultPose.trans(); + glm::vec3 tipPosition = headRigDefaultPose.trans(); + glm::vec3 spine2Position = rig.getAbsoluteDefaultPose(rig.indexOfJoint("Spine2")).trans(); + + glm::vec3 baseToTip = tipPosition - basePosition; + float baseToTipLength = glm::length(baseToTip); + glm::vec3 baseToTipNormal = baseToTip / baseToTipLength; + glm::vec3 baseToSpine2 = spine2Position - basePosition; + + _spine2SplineRatio = glm::dot(baseToSpine2, baseToTipNormal) / baseToTipLength; + + CubicHermiteSplineFunctorWithArcLength defaultSpline(headRigDefaultPose.rot(), headRigDefaultPose.trans(), hipsRigDefaultPose.rot(), hipsRigDefaultPose.trans()); + + // measure the total arc length along the spline + float totalDefaultArcLength = defaultSpline.arcLength(1.0f); + float t = defaultSpline.arcLengthInverse(_spine2SplineRatio * totalDefaultArcLength); + glm::vec3 defaultSplineSpine2Translation = defaultSpline(t); + + _spine2SplineOffset = spine2Position - defaultSplineSpine2Translation; + } + +} + void Avatar::clearUnscaledEyeHeightCache() { _unscaledEyeHeightCache.set(DEFAULT_AVATAR_EYE_HEIGHT); } +void Avatar::clearSpine2SplineRatioCache() { + _spine2SplineRatio = DEFAULT_AVATAR_EYE_HEIGHT; + _spine2SplineOffset = glm::vec3(); +} + float Avatar::getUnscaledEyeHeightFromSkeleton() const { // TODO: if performance becomes a concern we can cache this value rather then computing it everytime. @@ -2084,3 +2148,60 @@ void Avatar::clearAvatarGrabData(const QUuid& id) { } }); } + +uint32_t Avatar::appendSubMetaItems(render::ItemIDs& subItems) { + return _subItemLock.resultWithReadLock([&] { + uint32_t total = 0; + + if (_attachmentRenderIDs.size() > 0) { + subItems.insert(subItems.end(), _attachmentRenderIDs.begin(), _attachmentRenderIDs.end()); + total += (uint32_t)_attachmentRenderIDs.size(); + } + + if (_descendantRenderIDs.size() > 0) { + subItems.insert(subItems.end(), _descendantRenderIDs.begin(), _descendantRenderIDs.end()); + total += (uint32_t)_descendantRenderIDs.size(); + } + + return total; + }); +} + +void Avatar::updateAttachmentRenderIDs() { + _subItemLock.withWriteLock([&] { + _attachmentRenderIDs.clear(); + for (auto& attachmentModel : _attachmentModels) { + if (attachmentModel && attachmentModel->isRenderable()) { + auto& metaSubItems = attachmentModel->fetchRenderItemIDs(); + _attachmentRenderIDs.insert(_attachmentRenderIDs.end(), metaSubItems.begin(), metaSubItems.end()); + } + } + }); +} + +void Avatar::updateDescendantRenderIDs() { + _subItemLock.withWriteLock([&] { + _descendantRenderIDs.clear(); + auto entityTreeRenderer = DependencyManager::get(); + EntityTreePointer entityTree = entityTreeRenderer ? entityTreeRenderer->getTree() : nullptr; + if (entityTree) { + entityTree->withReadLock([&] { + forEachDescendant([&](SpatiallyNestablePointer object) { + if (object && object->getNestableType() == NestableType::Entity) { + EntityItemPointer entity = std::static_pointer_cast(object); + if (entity->isVisible()) { + auto renderer = entityTreeRenderer->renderableForEntityId(object->getID()); + if (renderer) { + render::ItemIDs renderableSubItems; + uint32_t numRenderableSubItems = renderer->metaFetchMetaSubItems(renderableSubItems); + if (numRenderableSubItems > 0) { + _descendantRenderIDs.insert(_descendantRenderIDs.end(), renderableSubItems.begin(), renderableSubItems.end()); + } + } + } + } + }); + }); + } + }); +} diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index aedfcedf89..cec440eb1a 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -266,6 +266,8 @@ public: * @returns {boolean} false. */ virtual bool setAbsoluteJointTranslationInObjectFrame(int index, const glm::vec3& translation) override { return false; } + virtual glm::vec3 getSpine2SplineOffset() const { return _spine2SplineOffset; } + virtual float getSpine2SplineRatio() const { return _spine2SplineRatio; } // world-space to avatar-space rigconversion functions /**jsdoc @@ -550,6 +552,8 @@ public: const std::vector& getMultiSphereShapes() const { return _multiSphereShapes; } void tearDownGrabs(); + uint32_t appendSubMetaItems(render::ItemIDs& subItems); + signals: /**jsdoc * Triggered when the avatar's target scale is changed. The target scale is the desired scale of the avatar without any @@ -626,7 +630,9 @@ public slots: protected: float getUnscaledEyeHeightFromSkeleton() const; void buildUnscaledEyeHeightCache(); + void buildSpine2SplineRatioCache(); void clearUnscaledEyeHeightCache(); + void clearSpine2SplineRatioCache(); virtual const QString& getSessionDisplayNameForTransport() const override { return _empty; } // Save a tiny bit of bandwidth. Mixer won't look at what we send. QString _empty{}; virtual void maybeUpdateSessionDisplayNameFromTransport(const QString& sessionDisplayName) override { _sessionDisplayName = sessionDisplayName; } // don't use no-op setter! @@ -703,8 +709,6 @@ protected: RateCounter<> _skeletonModelSimulationRate; RateCounter<> _jointDataSimulationRate; - -protected: class AvatarEntityDataHash { public: AvatarEntityDataHash(uint32_t h) : hash(h) {}; @@ -734,6 +738,8 @@ protected: float _displayNameAlpha { 1.0f }; ThreadSafeValueCache _unscaledEyeHeightCache { DEFAULT_AVATAR_EYE_HEIGHT }; + float _spine2SplineRatio { DEFAULT_SPINE2_SPLINE_PROPORTION }; + glm::vec3 _spine2SplineOffset; std::unordered_map _materials; std::mutex _materialsLock; @@ -764,6 +770,13 @@ protected: MapOfGrabs _avatarGrabs; SetOfIDs _grabsToChange; // updated grab IDs -- changes needed to entities or physics VectorOfIDs _grabsToDelete; // deleted grab IDs -- changes needed to entities or physics + + ReadWriteLockable _subItemLock; + void updateAttachmentRenderIDs(); + render::ItemIDs _attachmentRenderIDs; + void updateDescendantRenderIDs(); + render::ItemIDs _descendantRenderIDs; + uint32_t _lastAncestorChainRenderableVersion { 0 }; }; #endif // hifi_Avatar_h diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index 5f30d98ed6..c16d65506a 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -208,16 +208,16 @@ AvatarSharedPointer AvatarHashMap::addAvatar(const QUuid& sessionUUID, const QWe avatar->setSessionUUID(sessionUUID); avatar->setOwningAvatarMixer(mixerWeakPointer); - // addAvatar is only called from newOrExistingAvatar, which already locks _hashLock - _avatarHash.insert(sessionUUID, avatar); + { + QWriteLocker locker(&_hashLock); + _avatarHash.insert(sessionUUID, avatar); + } emit avatarAddedEvent(sessionUUID); return avatar; } -AvatarSharedPointer AvatarHashMap::newOrExistingAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer, - bool& isNew) { - QWriteLocker locker(&_hashLock); - auto avatar = _avatarHash.value(sessionUUID); +AvatarSharedPointer AvatarHashMap::newOrExistingAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer, bool& isNew) { + auto avatar = findAvatar(sessionUUID); if (!avatar) { avatar = addAvatar(sessionUUID, mixerWeakPointer); isNew = true; diff --git a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp index d8b8cbd54a..9828a8beda 100644 --- a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.cpp @@ -74,30 +74,15 @@ void Basic2DWindowOpenGLDisplayPlugin::customizeContext() { } } - - _virtualPadJumpBtnPixelSize = dpi * VirtualPad::Manager::JUMP_BTN_FULL_PIXELS / VirtualPad::Manager::DPI; - if (!_virtualPadJumpBtnTexture) { - auto iconPath = PathUtils::resourcesPath() + "images/fly.png"; - auto image = QImage(iconPath); - if (image.format() != QImage::Format_ARGB32) { - image = image.convertToFormat(QImage::Format_ARGB32); - } - if ((image.width() > 0) && (image.height() > 0)) { - image = image.scaled(_virtualPadJumpBtnPixelSize, _virtualPadJumpBtnPixelSize, Qt::KeepAspectRatio); - image = image.mirrored(); - - _virtualPadJumpBtnTexture = gpu::Texture::createStrict( - gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), - image.width(), image.height(), - gpu::Texture::MAX_NUM_MIPS, - gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR)); - _virtualPadJumpBtnTexture->setSource("virtualPad jump"); - auto usage = gpu::Texture::Usage::Builder().withColor().withAlpha(); - _virtualPadJumpBtnTexture->setUsage(usage.build()); - _virtualPadJumpBtnTexture->setStoredMipFormat(gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA)); - _virtualPadJumpBtnTexture->assignStoredMip(0, image.byteCount(), image.constBits()); - _virtualPadJumpBtnTexture->setAutoGenerateMips(true); - } + if (_virtualPadButtons.size() == 0) { + _virtualPadButtons.append(VirtualPadButton( + dpi * VirtualPad::Manager::BTN_FULL_PIXELS / VirtualPad::Manager::DPI, + PathUtils::resourcesPath() + "images/fly.png", + VirtualPad::Manager::Button::JUMP)); + _virtualPadButtons.append(VirtualPadButton( + dpi * VirtualPad::Manager::BTN_FULL_PIXELS / VirtualPad::Manager::DPI, + PathUtils::resourcesPath() + "images/handshake.png", + VirtualPad::Manager::Button::HANDSHAKE)); } #endif Parent::customizeContext(); @@ -133,8 +118,6 @@ void Basic2DWindowOpenGLDisplayPlugin::compositeExtra() { _virtualPadPixelSize, _virtualPadPixelSize); auto stickTransform = DependencyManager::get()->getPoint2DTransform(virtualPadManager.getLeftVirtualPad()->getCurrentTouch(), _virtualPadPixelSize, _virtualPadPixelSize); - auto jumpTransform = DependencyManager::get()->getPoint2DTransform(virtualPadManager.getJumpButtonPosition(), - _virtualPadJumpBtnPixelSize, _virtualPadJumpBtnPixelSize); render([&](gpu::Batch& batch) { batch.enableStereo(false); @@ -151,9 +134,9 @@ void Basic2DWindowOpenGLDisplayPlugin::compositeExtra() { batch.setModelTransform(stickTransform); batch.draw(gpu::TRIANGLE_STRIP, 4); - batch.setResourceTexture(0, _virtualPadJumpBtnTexture); - batch.setModelTransform(jumpTransform); - batch.draw(gpu::TRIANGLE_STRIP, 4); + foreach(VirtualPadButton virtualPadButton, _virtualPadButtons) { + virtualPadButton.draw(batch, virtualPadManager.getButtonPosition(virtualPadButton._button)); + } }); } #endif @@ -178,3 +161,47 @@ bool Basic2DWindowOpenGLDisplayPlugin::isThrottled() const { QScreen* Basic2DWindowOpenGLDisplayPlugin::getFullscreenTarget() { return qApp->primaryScreen(); } + +#if defined(Q_OS_ANDROID) + +Basic2DWindowOpenGLDisplayPlugin::VirtualPadButton::VirtualPadButton(qreal pixelSize, + QString iconPath, + VirtualPad::Manager::Button button) : + _pixelSize { pixelSize }, + _button { button } +{ + if (!_texture) { + auto image = QImage(iconPath); + if (image.format() != QImage::Format_ARGB32) { + image = image.convertToFormat(QImage::Format_ARGB32); + } + if ((image.width() > 0) && (image.height() > 0)) { + image = image.scaled(_pixelSize, _pixelSize, Qt::KeepAspectRatio); + image = image.mirrored(); + + _texture = gpu::Texture::createStrict( + gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), + image.width(), image.height(), + gpu::Texture::MAX_NUM_MIPS, + gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR)); + _texture->setSource(iconPath.toStdString()); + auto usage = gpu::Texture::Usage::Builder().withColor().withAlpha(); + _texture->setUsage(usage.build()); + _texture->setStoredMipFormat(gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA)); + _texture->assignStoredMip(0, image.byteCount(), image.constBits()); + _texture->setAutoGenerateMips(true); + } + } +} + +void Basic2DWindowOpenGLDisplayPlugin::VirtualPadButton::draw(gpu::Batch &batch, + glm::vec2 buttonPosition) { + auto transform = DependencyManager::get()->getPoint2DTransform( + buttonPosition, + _pixelSize, _pixelSize); + batch.setResourceTexture(0, _texture); + batch.setModelTransform(transform); + batch.draw(gpu::TRIANGLE_STRIP, 4); +} + +#endif \ No newline at end of file diff --git a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.h index 944d5e89d1..cc304c19c2 100644 --- a/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/Basic2DWindowOpenGLDisplayPlugin.h @@ -9,6 +9,10 @@ #include "OpenGLDisplayPlugin.h" +#if defined(Q_OS_ANDROID) +#include "VirtualPadManager.h" +#endif + const float TARGET_FRAMERATE_Basic2DWindowOpenGL = 60.0f; class QScreen; @@ -51,5 +55,23 @@ private: gpu::TexturePointer _virtualPadJumpBtnTexture; qreal _virtualPadJumpBtnPixelSize; + + gpu::TexturePointer _virtualPadRbBtnTexture; + qreal _virtualPadRbBtnPixelSize; + + class VirtualPadButton { + public: + + VirtualPadButton() {} + VirtualPadButton(qreal pixelSize, QString iconPath, VirtualPad::Manager::Button button); + + void draw(gpu::Batch& batch, glm::vec2 buttonPosition); + + gpu::TexturePointer _texture; + qreal _pixelSize; + VirtualPad::Manager::Button _button; + }; + QVector _virtualPadButtons; + #endif }; diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 20fc9a2290..c536e6b6e2 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -380,16 +380,26 @@ void OpenGLDisplayPlugin::customizeContext() { scissorState->setScissorEnable(true); { +#ifdef Q_OS_ANDROID + gpu::ShaderPointer program = gpu::Shader::createProgram(shader::gpu::program::DrawTextureGammaLinearToSRGB); +#else gpu::ShaderPointer program = gpu::Shader::createProgram(shader::gpu::program::DrawTexture); - _simplePipeline = gpu::Pipeline::create(program, scissorState); - _hudPipeline = gpu::Pipeline::create(program, blendState); +#endif + _simplePipeline = gpu::Pipeline::create(program, scissorState); } - { - gpu::ShaderPointer program = gpu::Shader::createProgram(shader::display_plugins::program::SrgbToLinear); +#ifdef Q_OS_ANDROID + gpu::ShaderPointer program = gpu::Shader::createProgram(shader::gpu::program::DrawTextureGammaLinearToSRGB); +#else + gpu::ShaderPointer program = gpu::Shader::createProgram(shader::gpu::program::DrawTextureGammaSRGBToLinear); +#endif _presentPipeline = gpu::Pipeline::create(program, scissorState); } + { + gpu::ShaderPointer program = gpu::Shader::createProgram(shader::gpu::program::DrawTexture); + _hudPipeline = gpu::Pipeline::create(program, blendState); + } { gpu::ShaderPointer program = gpu::Shader::createProgram(shader::gpu::program::DrawTextureMirroredX); _mirrorHUDPipeline = gpu::Pipeline::create(program, blendState); @@ -885,6 +895,7 @@ void OpenGLDisplayPlugin::updateCompositeFramebuffer() { auto renderSize = glm::uvec2(getRecommendedRenderSize()); if (!_compositeFramebuffer || _compositeFramebuffer->getSize() != renderSize) { _compositeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("OpenGLDisplayPlugin::composite", gpu::Element::COLOR_RGBA_32, renderSize.x, renderSize.y)); + // _compositeFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("OpenGLDisplayPlugin::composite", gpu::Element::COLOR_SRGBA_32, renderSize.x, renderSize.y)); } } diff --git a/libraries/display-plugins/src/display-plugins/SrgbToLinear.slf b/libraries/display-plugins/src/display-plugins/SrgbToLinear.slf deleted file mode 100644 index 8b324c81a5..0000000000 --- a/libraries/display-plugins/src/display-plugins/SrgbToLinear.slf +++ /dev/null @@ -1,22 +0,0 @@ -// OpenGLDisplayPlugin_present.frag - -LAYOUT(binding=0) uniform sampler2D colorMap; - -layout(location=0) in vec2 varTexCoord0; - -layout(location=0) out vec4 outFragColor; - -float sRGBFloatToLinear(float value) { - const float SRGB_ELBOW = 0.04045; - - return mix(pow((value + 0.055) / 1.055, 2.4), value / 12.92, float(value <= SRGB_ELBOW)); -} - -vec3 colorToLinearRGB(vec3 srgb) { - return vec3(sRGBFloatToLinear(srgb.r), sRGBFloatToLinear(srgb.g), sRGBFloatToLinear(srgb.b)); -} - -void main(void) { - outFragColor.a = 1.0; - outFragColor.rgb = colorToLinearRGB(texture(colorMap, varTexCoord0).rgb); -} diff --git a/libraries/display-plugins/src/display-plugins/SrgbToLinear.slp b/libraries/display-plugins/src/display-plugins/SrgbToLinear.slp deleted file mode 100644 index c2c4bfbebd..0000000000 --- a/libraries/display-plugins/src/display-plugins/SrgbToLinear.slp +++ /dev/null @@ -1 +0,0 @@ -VERTEX gpu::vertex::DrawUnitQuadTexcoord diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 319acc750f..143c7fa377 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -73,44 +73,44 @@ EntityTreeRenderer::EntityTreeRenderer(bool wantScripts, AbstractViewStateInterf _currentHoverOverEntityID = UNKNOWN_ENTITY_ID; _currentClickingOnEntityID = UNKNOWN_ENTITY_ID; - auto entityScriptingInterface = DependencyManager::get(); + auto entityScriptingInterface = DependencyManager::get().data(); auto pointerManager = DependencyManager::get(); - connect(pointerManager.data(), &PointerManager::hoverBeginEntity, entityScriptingInterface.data(), &EntityScriptingInterface::hoverEnterEntity); - connect(pointerManager.data(), &PointerManager::hoverContinueEntity, entityScriptingInterface.data(), &EntityScriptingInterface::hoverOverEntity); - connect(pointerManager.data(), &PointerManager::hoverEndEntity, entityScriptingInterface.data(), &EntityScriptingInterface::hoverLeaveEntity); - connect(pointerManager.data(), &PointerManager::triggerBeginEntity, entityScriptingInterface.data(), &EntityScriptingInterface::mousePressOnEntity); - connect(pointerManager.data(), &PointerManager::triggerContinueEntity, entityScriptingInterface.data(), &EntityScriptingInterface::mouseMoveOnEntity); - connect(pointerManager.data(), &PointerManager::triggerEndEntity, entityScriptingInterface.data(), &EntityScriptingInterface::mouseReleaseOnEntity); + connect(pointerManager.data(), &PointerManager::hoverBeginEntity, entityScriptingInterface, &EntityScriptingInterface::hoverEnterEntity); + connect(pointerManager.data(), &PointerManager::hoverContinueEntity, entityScriptingInterface, &EntityScriptingInterface::hoverOverEntity); + connect(pointerManager.data(), &PointerManager::hoverEndEntity, entityScriptingInterface, &EntityScriptingInterface::hoverLeaveEntity); + connect(pointerManager.data(), &PointerManager::triggerBeginEntity, entityScriptingInterface, &EntityScriptingInterface::mousePressOnEntity); + connect(pointerManager.data(), &PointerManager::triggerContinueEntity, entityScriptingInterface, &EntityScriptingInterface::mouseMoveOnEntity); + connect(pointerManager.data(), &PointerManager::triggerEndEntity, entityScriptingInterface, &EntityScriptingInterface::mouseReleaseOnEntity); // Forward mouse events to web entities auto handlePointerEvent = [&](const QUuid& entityID, const PointerEvent& event) { std::shared_ptr thisEntity; auto entity = getEntity(entityID); - if (entity && entity->getType() == EntityTypes::Web) { + if (entity && entity->isVisible() && entity->getType() == EntityTypes::Web) { thisEntity = std::static_pointer_cast(renderableForEntityId(entityID)); } if (thisEntity) { QMetaObject::invokeMethod(thisEntity.get(), "handlePointerEvent", Q_ARG(const PointerEvent&, event)); } }; - connect(entityScriptingInterface.data(), &EntityScriptingInterface::mousePressOnEntity, this, handlePointerEvent); - connect(entityScriptingInterface.data(), &EntityScriptingInterface::mouseMoveOnEntity, this, handlePointerEvent); - connect(entityScriptingInterface.data(), &EntityScriptingInterface::mouseReleaseOnEntity, this, handlePointerEvent); - connect(entityScriptingInterface.data(), &EntityScriptingInterface::hoverEnterEntity, this, [&](const QUuid& entityID, const PointerEvent& event) { + connect(entityScriptingInterface, &EntityScriptingInterface::mousePressOnEntity, this, handlePointerEvent); + connect(entityScriptingInterface, &EntityScriptingInterface::mouseMoveOnEntity, this, handlePointerEvent); + connect(entityScriptingInterface, &EntityScriptingInterface::mouseReleaseOnEntity, this, handlePointerEvent); + connect(entityScriptingInterface, &EntityScriptingInterface::hoverEnterEntity, this, [&](const QUuid& entityID, const PointerEvent& event) { std::shared_ptr thisEntity; auto entity = getEntity(entityID); - if (entity && entity->getType() == EntityTypes::Web) { + if (entity && entity->isVisible() && entity->getType() == EntityTypes::Web) { thisEntity = std::static_pointer_cast(renderableForEntityId(entityID)); } if (thisEntity) { QMetaObject::invokeMethod(thisEntity.get(), "hoverEnterEntity", Q_ARG(const PointerEvent&, event)); } }); - connect(entityScriptingInterface.data(), &EntityScriptingInterface::hoverOverEntity, this, handlePointerEvent); - connect(entityScriptingInterface.data(), &EntityScriptingInterface::hoverLeaveEntity, this, [&](const QUuid& entityID, const PointerEvent& event) { + connect(entityScriptingInterface, &EntityScriptingInterface::hoverOverEntity, this, handlePointerEvent); + connect(entityScriptingInterface, &EntityScriptingInterface::hoverLeaveEntity, this, [&](const QUuid& entityID, const PointerEvent& event) { std::shared_ptr thisEntity; auto entity = getEntity(entityID); - if (entity && entity->getType() == EntityTypes::Web) { + if (entity && entity->isVisible() && entity->getType() == EntityTypes::Web) { thisEntity = std::static_pointer_cast(renderableForEntityId(entityID)); } if (thisEntity) { @@ -196,8 +196,8 @@ void EntityTreeRenderer::resetEntitiesScriptEngine() { }); } -void EntityTreeRenderer::stopNonLocalEntityScripts() { - leaveNonLocalEntities(); +void EntityTreeRenderer::stopDomainAndNonOwnedEntities() { + leaveDomainAndNonOwnedEntities(); // unload and stop the engine if (_entitiesScriptEngine) { QList entitiesWithEntityScripts = _entitiesScriptEngine->getListOfEntityScriptIDs(); @@ -206,7 +206,7 @@ void EntityTreeRenderer::stopNonLocalEntityScripts() { EntityItemPointer entityItem = getTree()->findEntityByEntityItemID(entityID); if (entityItem) { - if (!entityItem->isLocalEntity()) { + if (!(entityItem->isLocalEntity() || (entityItem->isAvatarEntity() && entityItem->getOwningAvatarID() == getTree()->getMyAvatarSessionUUID()))) { _entitiesScriptEngine->unloadEntityScript(entityID, true); } } @@ -214,27 +214,24 @@ void EntityTreeRenderer::stopNonLocalEntityScripts() { } } -void EntityTreeRenderer::clearNonLocalEntities() { - stopNonLocalEntityScripts(); +void EntityTreeRenderer::clearDomainAndNonOwnedEntities() { + stopDomainAndNonOwnedEntities(); std::unordered_map savedEntities; // remove all entities from the scene - _space->clear(); auto scene = _viewState->getMain3DScene(); if (scene) { render::Transaction transaction; for (const auto& entry : _entitiesInScene) { const auto& renderer = entry.second; const EntityItemPointer& entityItem = renderer->getEntity(); - if (!entityItem->isLocalEntity()) { + if (!(entityItem->isLocalEntity() || (entityItem->isAvatarEntity() && entityItem->getOwningAvatarID() == getTree()->getMyAvatarSessionUUID()))) { renderer->removeFromScene(scene, transaction); } else { savedEntities[entry.first] = entry.second; } } scene->enqueueTransaction(transaction); - } else { - qCWarning(entitiesrenderer) << "EntitityTreeRenderer::clear(), Unexpected null scene, possibly during application shutdown"; } _renderablesToUpdate = savedEntities; @@ -242,7 +239,7 @@ void EntityTreeRenderer::clearNonLocalEntities() { _layeredZones.clearNonLocalLayeredZones(); - OctreeProcessor::clearNonLocalEntities(); + OctreeProcessor::clearDomainAndNonOwnedEntities(); } void EntityTreeRenderer::clear() { @@ -259,8 +256,6 @@ void EntityTreeRenderer::clear() { resetEntitiesScriptEngine(); } // remove all entities from the scene - - _space->clear(); auto scene = _viewState->getMain3DScene(); if (scene) { render::Transaction transaction; @@ -660,22 +655,22 @@ bool EntityTreeRenderer::checkEnterLeaveEntities() { return didUpdate; } -void EntityTreeRenderer::leaveNonLocalEntities() { +void EntityTreeRenderer::leaveDomainAndNonOwnedEntities() { if (_tree && !_shuttingDown) { - QVector currentLocalEntitiesInside; + QVector currentEntitiesInsideToSave; foreach (const EntityItemID& entityID, _currentEntitiesInside) { EntityItemPointer entityItem = getTree()->findEntityByEntityItemID(entityID); - if (!entityItem->isLocalEntity()) { + if (!(entityItem->isLocalEntity() || (entityItem->isAvatarEntity() && entityItem->getOwningAvatarID() == getTree()->getMyAvatarSessionUUID()))) { emit leaveEntity(entityID); if (_entitiesScriptEngine) { _entitiesScriptEngine->callEntityScriptMethod(entityID, "leaveEntity"); } } else { - currentLocalEntitiesInside.push_back(entityID); + currentEntitiesInsideToSave.push_back(entityID); } } - _currentEntitiesInside = currentLocalEntitiesInside; + _currentEntitiesInside = currentEntitiesInsideToSave; forceRecheckEntities(); } } @@ -797,11 +792,11 @@ static PointerEvent::Button toPointerButton(const QMouseEvent& event) { } } -std::pair EntityTreeRenderer::mousePressEvent(QMouseEvent* event) { +QUuid EntityTreeRenderer::mousePressEvent(QMouseEvent* event) { // If we don't have a tree, or we're in the process of shutting down, then don't // process these events. if (!_tree || _shuttingDown) { - return { FLT_MAX, UNKNOWN_ENTITY_ID }; + return UNKNOWN_ENTITY_ID; } PerformanceTimer perfTimer("EntityTreeRenderer::mousePressEvent"); @@ -810,11 +805,13 @@ std::pair EntityTreeRenderer::mousePressEvent(QMouseEvent* event) RayToEntityIntersectionResult rayPickResult = _getPrevRayPickResultOperator(_mouseRayPickID); EntityItemPointer entity; if (rayPickResult.intersects && (entity = getTree()->findEntityByID(rayPickResult.entityID))) { - auto properties = entity->getProperties(); - QString urlString = properties.getHref(); - QUrl url = QUrl(urlString, QUrl::StrictMode); - if (url.isValid() && !url.isEmpty()){ - DependencyManager::get()->handleLookupString(urlString); + if (!EntityTree::areEntityClicksCaptured()) { + auto properties = entity->getProperties(); + QString urlString = properties.getHref(); + QUrl url = QUrl(urlString, QUrl::StrictMode); + if (url.isValid() && !url.isEmpty()) { + DependencyManager::get()->handleLookupString(urlString); + } } glm::vec2 pos2D = projectOntoEntityXYPlane(entity, ray, rayPickResult); @@ -832,10 +829,10 @@ std::pair EntityTreeRenderer::mousePressEvent(QMouseEvent* event) _lastPointerEvent = pointerEvent; _lastPointerEventValid = true; - return { rayPickResult.distance, rayPickResult.entityID }; + return rayPickResult.entityID; } emit entityScriptingInterface->mousePressOffEntity(); - return { FLT_MAX, UNKNOWN_ENTITY_ID }; + return UNKNOWN_ENTITY_ID; } void EntityTreeRenderer::mouseDoublePressEvent(QMouseEvent* event) { @@ -1360,3 +1357,36 @@ EntityEditPacketSender* EntityTreeRenderer::getPacketSender() { EntityEditPacketSender* packetSender = peSimulation ? peSimulation->getPacketSender() : nullptr; return packetSender; } + +std::function EntityTreeRenderer::_addMaterialToEntityOperator = nullptr; +std::function EntityTreeRenderer::_removeMaterialFromEntityOperator = nullptr; +std::function EntityTreeRenderer::_addMaterialToAvatarOperator = nullptr; +std::function EntityTreeRenderer::_removeMaterialFromAvatarOperator = nullptr; + +bool EntityTreeRenderer::addMaterialToEntity(const QUuid& entityID, graphics::MaterialLayer material, const std::string& parentMaterialName) { + if (_addMaterialToEntityOperator) { + return _addMaterialToEntityOperator(entityID, material, parentMaterialName); + } + return false; +} + +bool EntityTreeRenderer::removeMaterialFromEntity(const QUuid& entityID, graphics::MaterialPointer material, const std::string& parentMaterialName) { + if (_removeMaterialFromEntityOperator) { + return _removeMaterialFromEntityOperator(entityID, material, parentMaterialName); + } + return false; +} + +bool EntityTreeRenderer::addMaterialToAvatar(const QUuid& avatarID, graphics::MaterialLayer material, const std::string& parentMaterialName) { + if (_addMaterialToAvatarOperator) { + return _addMaterialToAvatarOperator(avatarID, material, parentMaterialName); + } + return false; +} + +bool EntityTreeRenderer::removeMaterialFromAvatar(const QUuid& avatarID, graphics::MaterialPointer material, const std::string& parentMaterialName) { + if (_removeMaterialFromAvatarOperator) { + return _removeMaterialFromAvatarOperator(avatarID, material, parentMaterialName); + } + return false; +} diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index 204dc50c45..a257951ba8 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -73,6 +73,7 @@ public: static void setEntityLoadingPriorityFunction(CalculateEntityLoadingPriority fn) { _calculateEntityLoadingPriorityFunc = fn; } void setMouseRayPickID(unsigned int rayPickID) { _mouseRayPickID = rayPickID; } + unsigned int getMouseRayPickID() { return _mouseRayPickID; } void setMouseRayPickResultOperator(std::function getPrevRayPickResultOperator) { _getPrevRayPickResultOperator = getPrevRayPickResultOperator; } void setSetPrecisionPickingOperator(std::function setPrecisionPickingOperator) { _setPrecisionPickingOperator = setPrecisionPickingOperator; } @@ -86,14 +87,14 @@ public: virtual void init() override; /// clears the tree - virtual void clearNonLocalEntities() override; + virtual void clearDomainAndNonOwnedEntities() override; virtual void clear() override; /// reloads the entity scripts, calling unload and preload void reloadEntityScripts(); // event handles which may generate entity related events - std::pair mousePressEvent(QMouseEvent* event); + QUuid mousePressEvent(QMouseEvent* event); void mouseReleaseEvent(QMouseEvent* event); void mouseDoublePressEvent(QMouseEvent* event); void mouseMoveEvent(QMouseEvent* event); @@ -120,6 +121,16 @@ public: EntityEditPacketSender* getPacketSender(); + static void setAddMaterialToEntityOperator(std::function addMaterialToEntityOperator) { _addMaterialToEntityOperator = addMaterialToEntityOperator; } + static void setRemoveMaterialFromEntityOperator(std::function removeMaterialFromEntityOperator) { _removeMaterialFromEntityOperator = removeMaterialFromEntityOperator; } + static bool addMaterialToEntity(const QUuid& entityID, graphics::MaterialLayer material, const std::string& parentMaterialName); + static bool removeMaterialFromEntity(const QUuid& entityID, graphics::MaterialPointer material, const std::string& parentMaterialName); + + static void setAddMaterialToAvatarOperator(std::function addMaterialToAvatarOperator) { _addMaterialToAvatarOperator = addMaterialToAvatarOperator; } + static void setRemoveMaterialFromAvatarOperator(std::function removeMaterialFromAvatarOperator) { _removeMaterialFromAvatarOperator = removeMaterialFromAvatarOperator; } + static bool addMaterialToAvatar(const QUuid& avatarID, graphics::MaterialLayer material, const std::string& parentMaterialName); + static bool removeMaterialFromAvatar(const QUuid& avatarID, graphics::MaterialPointer material, const std::string& parentMaterialName); + signals: void enterEntity(const EntityItemID& entityItemID); void leaveEntity(const EntityItemID& entityItemID); @@ -159,7 +170,7 @@ private: bool findBestZoneAndMaybeContainingEntities(QVector* entitiesContainingAvatar = nullptr); bool applyLayeredZones(); - void stopNonLocalEntityScripts(); + void stopDomainAndNonOwnedEntities(); void checkAndCallPreload(const EntityItemID& entityID, bool reload = false, bool unloadFirst = false); @@ -168,7 +179,7 @@ private: QScriptValueList createEntityArgs(const EntityItemID& entityID); bool checkEnterLeaveEntities(); - void leaveNonLocalEntities(); + void leaveDomainAndNonOwnedEntities(); void leaveAllEntities(); void forceRecheckEntities(); @@ -255,6 +266,11 @@ private: workload::SpacePointer _space{ new workload::Space() }; workload::Transaction::Updates _spaceUpdates; + static std::function _addMaterialToEntityOperator; + static std::function _removeMaterialFromEntityOperator; + static std::function _addMaterialToAvatarOperator; + static std::function _removeMaterialFromAvatarOperator; + }; diff --git a/libraries/entities-renderer/src/RenderableEntityItem.cpp b/libraries/entities-renderer/src/RenderableEntityItem.cpp index 83f0bdcff3..a6826da91b 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableEntityItem.cpp @@ -146,7 +146,6 @@ EntityRenderer::EntityRenderer(const EntityItemPointer& entity) : _created(entit _needsRenderUpdate = true; emit requestRenderUpdate(); }); - _materials = entity->getMaterials(); } EntityRenderer::~EntityRenderer() { } @@ -321,6 +320,7 @@ bool EntityRenderer::addToScene(const ScenePointer& scene, Transaction& transact transaction.resetItem(_renderItemID, renderPayload); onAddToScene(_entity); updateInScene(scene, transaction); + _entity->bumpAncestorChainRenderableVersion(); return true; } @@ -328,6 +328,7 @@ void EntityRenderer::removeFromScene(const ScenePointer& scene, Transaction& tra onRemoveFromScene(_entity); transaction.removeItem(_renderItemID); Item::clearID(_renderItemID); + _entity->bumpAncestorChainRenderableVersion(); } void EntityRenderer::updateInScene(const ScenePointer& scene, Transaction& transaction) { @@ -353,14 +354,6 @@ void EntityRenderer::updateInScene(const ScenePointer& scene, Transaction& trans }); } -void EntityRenderer::clearSubRenderItemIDs() { - _subRenderItemIDs.clear(); -} - -void EntityRenderer::setSubRenderItemIDs(const render::ItemIDs& ids) { - _subRenderItemIDs = ids; -} - // // Internal methods // diff --git a/libraries/entities-renderer/src/RenderableEntityItem.h b/libraries/entities-renderer/src/RenderableEntityItem.h index d5e236a76a..e9a6035e3d 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.h +++ b/libraries/entities-renderer/src/RenderableEntityItem.h @@ -52,9 +52,6 @@ public: virtual bool addToScene(const ScenePointer& scene, Transaction& transaction) final; virtual void removeFromScene(const ScenePointer& scene, Transaction& transaction); - void clearSubRenderItemIDs(); - void setSubRenderItemIDs(const render::ItemIDs& ids); - const uint64_t& getUpdateTime() const { return _updateTime; } virtual void addMaterial(graphics::MaterialLayer material, const std::string& parentMaterialName); @@ -64,6 +61,9 @@ public: static glm::vec4 calculatePulseColor(const glm::vec4& color, const PulsePropertyGroup& pulseProperties, quint64 start); + virtual uint32_t metaFetchMetaSubItems(ItemIDs& subItems) override; + virtual Item::Bound getBound() override; + protected: virtual bool needsRenderUpdateFromEntity() const final { return needsRenderUpdateFromEntity(_entity); } virtual void onAddToScene(const EntityItemPointer& entity); @@ -75,9 +75,7 @@ protected: // Implementing the PayloadProxyInterface methods virtual ItemKey getKey() override; virtual ShapeKey getShapeKey() override; - virtual Item::Bound getBound() override; virtual void render(RenderArgs* args) override final; - virtual uint32_t metaFetchMetaSubItems(ItemIDs& subItems) override; virtual render::hifi::Tag getTagMask() const; virtual render::hifi::Layer getHifiRenderLayer() const; @@ -133,7 +131,6 @@ protected: SharedSoundPointer _collisionSound; QUuid _changeHandlerId; ItemID _renderItemID{ Item::INVALID_ITEM_ID }; - ItemIDs _subRenderItemIDs; uint64_t _fadeStartTime{ usecTimestampNow() }; uint64_t _updateTime{ usecTimestampNow() }; // used when sorting/throttling render updates bool _isFading { EntityTreeRenderer::getEntitiesShouldFadeFunction()() }; diff --git a/libraries/entities-renderer/src/RenderableImageEntityItem.cpp b/libraries/entities-renderer/src/RenderableImageEntityItem.cpp index 96dd1733e7..6638bc0687 100644 --- a/libraries/entities-renderer/src/RenderableImageEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableImageEntityItem.cpp @@ -170,7 +170,7 @@ void ImageEntityRenderer::doRender(RenderArgs* args) { Q_ASSERT(args->_batch); gpu::Batch* batch = args->_batch; - transform.setRotation(EntityItem::getBillboardRotation(transform.getTranslation(), transform.getRotation(), _billboardMode)); + transform.setRotation(EntityItem::getBillboardRotation(transform.getTranslation(), transform.getRotation(), _billboardMode, args->getViewFrustum().getPosition())); transform.postScale(dimensions); batch->setModelTransform(transform); diff --git a/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp b/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp index 483f9ffe1c..2eb877b0e1 100644 --- a/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp @@ -14,42 +14,210 @@ using namespace render; using namespace render::entities; -bool MaterialEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const { - if (entity->getMaterial() != _drawMaterial) { - return true; - } - if (entity->getParentID() != _parentID) { - return true; - } - if (entity->getMaterialMappingPos() != _materialMappingPos || entity->getMaterialMappingScale() != _materialMappingScale || entity->getMaterialMappingRot() != _materialMappingRot) { +bool MaterialEntityRenderer::needsRenderUpdate() const { + if (_retryApply) { return true; } if (!_texturesLoaded) { return true; } + return Parent::needsRenderUpdate(); +} + +bool MaterialEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const { + if (resultWithReadLock([&] { + if (entity->getMaterialMappingMode() != _materialMappingMode) { + return true; + } + if (entity->getMaterialRepeat() != _materialRepeat) { + return true; + } + if (entity->getMaterialMappingPos() != _materialMappingPos || entity->getMaterialMappingScale() != _materialMappingScale || entity->getMaterialMappingRot() != _materialMappingRot) { + return true; + } + if (entity->getTransform() != _transform) { + return true; + } + if (entity->getUnscaledDimensions() != _dimensions) { + return true; + } + + if (entity->getMaterialURL() != _materialURL) { + return true; + } + if (entity->getMaterialData() != _materialData) { + return true; + } + if (entity->getParentMaterialName() != _parentMaterialName) { + return true; + } + if (entity->getParentID() != _parentID) { + return true; + } + if (entity->getPriority() != _priority) { + return true; + } + + return false; + })) { + return true; + } return false; } -void MaterialEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) { +void MaterialEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) { withWriteLock([&] { - if (_drawMaterial != entity->getMaterial()) { - _texturesLoaded = false; - _drawMaterial = entity->getMaterial(); + bool deleteNeeded = false; + bool addNeeded = _retryApply; + bool transformChanged = false; + { + MaterialMappingMode mode = entity->getMaterialMappingMode(); + if (mode != _materialMappingMode) { + _materialMappingMode = mode; + transformChanged = true; + } } - _parentID = entity->getParentID(); - _materialMappingPos = entity->getMaterialMappingPos(); - _materialMappingScale = entity->getMaterialMappingScale(); - _materialMappingRot = entity->getMaterialMappingRot(); + { + bool repeat = entity->getMaterialRepeat(); + if (repeat != _materialRepeat) { + _materialRepeat = repeat; + transformChanged = true; + } + } + { + glm::vec2 mappingPos = entity->getMaterialMappingPos(); + glm::vec2 mappingScale = entity->getMaterialMappingScale(); + float mappingRot = entity->getMaterialMappingRot(); + if (mappingPos != _materialMappingPos || mappingScale != _materialMappingScale || mappingRot != _materialMappingRot) { + _materialMappingPos = mappingPos; + _materialMappingScale = mappingScale; + _materialMappingRot = mappingRot; + transformChanged |= _materialMappingMode == MaterialMappingMode::UV; + } + } + { + Transform transform = entity->getTransform(); + glm::vec3 dimensions = entity->getUnscaledDimensions(); + if (transform != _transform || dimensions != _dimensions) { + _transform = transform; + _dimensions = dimensions; + transformChanged |= _materialMappingMode == MaterialMappingMode::PROJECTED; + } + } + + { + auto material = getMaterial(); + // Update the old material regardless of if it's going to change + if (transformChanged && material && !_parentID.isNull()) { + deleteNeeded = true; + addNeeded = true; + applyTextureTransform(material); + } + } + + bool urlChanged = false; + std::string newCurrentMaterialName = _currentMaterialName; + { + QString materialURL = entity->getMaterialURL(); + if (materialURL != _materialURL) { + _materialURL = materialURL; + if (_materialURL.contains("?")) { + auto split = _materialURL.split("?"); + newCurrentMaterialName = split.last().toStdString(); + } + urlChanged = true; + } + } + + bool usingMaterialData = _materialURL.startsWith("materialData"); + bool materialDataChanged = false; + QUuid oldParentID = _parentID; + QString oldParentMaterialName = _parentMaterialName; + { + QString materialData = entity->getMaterialData(); + if (materialData != _materialData) { + _materialData = materialData; + if (usingMaterialData) { + materialDataChanged = true; + } + } + } + { + QString parentMaterialName = entity->getParentMaterialName(); + if (parentMaterialName != _parentMaterialName) { + _parentMaterialName = parentMaterialName; + deleteNeeded = true; + addNeeded = true; + } + } + { + QUuid parentID = entity->getParentID(); + if (parentID != _parentID) { + _parentID = parentID; + deleteNeeded = true; + addNeeded = true; + } + } + { + quint16 priority = entity->getPriority(); + if (priority != _priority) { + _priority = priority; + deleteNeeded = true; + addNeeded = true; + } + } + + if (urlChanged && !usingMaterialData) { + _networkMaterial = MaterialCache::instance().getMaterial(_materialURL); + auto onMaterialRequestFinished = [&, oldParentID, oldParentMaterialName, newCurrentMaterialName](bool success) { + if (success) { + deleteMaterial(oldParentID, oldParentMaterialName); + _texturesLoaded = false; + _parsedMaterials = _networkMaterial->parsedMaterials; + setCurrentMaterialName(newCurrentMaterialName); + applyMaterial(); + } else { + deleteMaterial(oldParentID, oldParentMaterialName); + _retryApply = false; + _texturesLoaded = true; + } + }; + if (_networkMaterial) { + if (_networkMaterial->isLoaded()) { + onMaterialRequestFinished(!_networkMaterial->isFailed()); + } else { + connect(_networkMaterial.data(), &Resource::finished, this, onMaterialRequestFinished); + } + } + } else if (materialDataChanged && usingMaterialData) { + deleteMaterial(oldParentID, oldParentMaterialName); + _texturesLoaded = false; + _parsedMaterials = NetworkMaterialResource::parseJSONMaterials(QJsonDocument::fromJson(_materialData.toUtf8()), _materialURL); + // Since our material changed, the current name might not be valid anymore, so we need to update + setCurrentMaterialName(newCurrentMaterialName); + applyMaterial(); + } else { + if (deleteNeeded) { + deleteMaterial(oldParentID, oldParentMaterialName); + } + if (addNeeded) { + applyMaterial(); + } + } + + { + auto material = getMaterial(); + bool newTexturesLoaded = material ? !material->isMissingTexture() : false; + if (!_texturesLoaded && newTexturesLoaded) { + material->checkResetOpacityMap(); + } + _texturesLoaded = newTexturesLoaded; + } + _renderTransform = getModelTransform(); const float MATERIAL_ENTITY_SCALE = 0.5f; _renderTransform.postScale(MATERIAL_ENTITY_SCALE); _renderTransform.postScale(ENTITY_ITEM_DEFAULT_DIMENSIONS); - - bool newTexturesLoaded = _drawMaterial ? !_drawMaterial->isMissingTexture() : false; - if (!_texturesLoaded && newTexturesLoaded) { - _drawMaterial->checkResetOpacityMap(); - } - _texturesLoaded = newTexturesLoaded; }); } @@ -61,8 +229,9 @@ ItemKey MaterialEntityRenderer::getKey() { builder.withInvisible(); } - if (_drawMaterial) { - auto matKey = _drawMaterial->getKey(); + const auto drawMaterial = getMaterial(); + if (drawMaterial) { + auto matKey = drawMaterial->getKey(); if (matKey.isTranslucent()) { builder.withTransparent(); } @@ -73,8 +242,9 @@ ItemKey MaterialEntityRenderer::getKey() { ShapeKey MaterialEntityRenderer::getShapeKey() { graphics::MaterialKey drawMaterialKey; - if (_drawMaterial) { - drawMaterialKey = _drawMaterial->getKey(); + const auto drawMaterial = getMaterial(); + if (drawMaterial) { + drawMaterialKey = drawMaterial->getKey(); } bool isTranslucent = drawMaterialKey.isTranslucent(); @@ -112,18 +282,24 @@ void MaterialEntityRenderer::doRender(RenderArgs* args) { // Don't render if our parent is set or our material is null QUuid parentID; + withReadLock([&] { + parentID = _parentID; + }); + if (!parentID.isNull()) { + return; + } + Transform renderTransform; graphics::MaterialPointer drawMaterial; Transform textureTransform; withReadLock([&] { - parentID = _parentID; renderTransform = _renderTransform; - drawMaterial = _drawMaterial; + drawMaterial = getMaterial(); textureTransform.setTranslation(glm::vec3(_materialMappingPos, 0)); textureTransform.setRotation(glm::vec3(0, 0, glm::radians(_materialMappingRot))); textureTransform.setScale(glm::vec3(_materialMappingScale, 1)); }); - if (!parentID.isNull() || !drawMaterial) { + if (!drawMaterial) { return; } @@ -142,3 +318,86 @@ void MaterialEntityRenderer::doRender(RenderArgs* args) { args->_details._trianglesRendered += (int)DependencyManager::get()->getSphereTriangleCount(); } + +void MaterialEntityRenderer::setCurrentMaterialName(const std::string& currentMaterialName) { + if (_parsedMaterials.networkMaterials.find(currentMaterialName) != _parsedMaterials.networkMaterials.end()) { + _currentMaterialName = currentMaterialName; + } else if (_parsedMaterials.names.size() > 0) { + _currentMaterialName = _parsedMaterials.names[0]; + } +} + +std::shared_ptr MaterialEntityRenderer::getMaterial() const { + auto material = _parsedMaterials.networkMaterials.find(_currentMaterialName); + if (material != _parsedMaterials.networkMaterials.end()) { + return material->second; + } else { + return nullptr; + } +} + +void MaterialEntityRenderer::deleteMaterial(const QUuid& oldParentID, const QString& oldParentMaterialName) { + std::shared_ptr material = _appliedMaterial; + if (!material || oldParentID.isNull()) { + return; + } + + // Our parent could be an entity or an avatar + std::string oldParentMaterialNameStd = oldParentMaterialName.toStdString(); + if (EntityTreeRenderer::removeMaterialFromEntity(oldParentID, material, oldParentMaterialNameStd)) { + _appliedMaterial = nullptr; + return; + } + + if (EntityTreeRenderer::removeMaterialFromAvatar(oldParentID, material, oldParentMaterialNameStd)) { + _appliedMaterial = nullptr; + return; + } + + // if a remove fails, our parent is gone, so we don't need to retry +} + +void MaterialEntityRenderer::applyTextureTransform(std::shared_ptr& material) { + Transform textureTransform; + if (_materialMappingMode == MaterialMappingMode::UV) { + textureTransform.setTranslation(glm::vec3(_materialMappingPos, 0.0f)); + textureTransform.setRotation(glm::vec3(0.0f, 0.0f, glm::radians(_materialMappingRot))); + textureTransform.setScale(glm::vec3(_materialMappingScale, 1.0f)); + } else if (_materialMappingMode == MaterialMappingMode::PROJECTED) { + textureTransform = _transform; + textureTransform.postScale(_dimensions); + // Pass the inverse transform here so we don't need to compute it in the shaders + textureTransform.evalFromRawMatrix(textureTransform.getInverseMatrix()); + } + material->setTextureTransforms(textureTransform, _materialMappingMode, _materialRepeat); +} + +void MaterialEntityRenderer::applyMaterial() { + _retryApply = false; + + std::shared_ptr material = getMaterial(); + QUuid parentID = _parentID; + if (!material || parentID.isNull()) { + _appliedMaterial = nullptr; + return; + } + + applyTextureTransform(material); + + graphics::MaterialLayer materialLayer = graphics::MaterialLayer(material, _priority); + + // Our parent could be an entity or an avatar + std::string parentMaterialName = _parentMaterialName.toStdString(); + if (EntityTreeRenderer::addMaterialToEntity(parentID, materialLayer, parentMaterialName)) { + _appliedMaterial = material; + return; + } + + if (EntityTreeRenderer::addMaterialToAvatar(parentID, materialLayer, parentMaterialName)) { + _appliedMaterial = material; + return; + } + + // if we've reached this point, we couldn't find our parent, so we need to try again later + _retryApply = true; +} \ No newline at end of file diff --git a/libraries/entities-renderer/src/RenderableMaterialEntityItem.h b/libraries/entities-renderer/src/RenderableMaterialEntityItem.h index c90048ecf5..d714727c7a 100644 --- a/libraries/entities-renderer/src/RenderableMaterialEntityItem.h +++ b/libraries/entities-renderer/src/RenderableMaterialEntityItem.h @@ -13,6 +13,8 @@ #include +#include + class NetworkMaterial; namespace render { namespace entities { @@ -22,22 +24,46 @@ class MaterialEntityRenderer : public TypedEntityRenderer { using Pointer = std::shared_ptr; public: MaterialEntityRenderer(const EntityItemPointer& entity) : Parent(entity) {} + ~MaterialEntityRenderer() { deleteMaterial(_parentID, _parentMaterialName); } private: + virtual bool needsRenderUpdate() const override; virtual bool needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const override; - virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override; + virtual void doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) override; virtual void doRender(RenderArgs* args) override; ItemKey getKey() override; ShapeKey getShapeKey() override; + QString _materialURL; + QString _materialData; + QString _parentMaterialName; + quint16 _priority; QUuid _parentID; + + MaterialMappingMode _materialMappingMode; + bool _materialRepeat; glm::vec2 _materialMappingPos; glm::vec2 _materialMappingScale; float _materialMappingRot; - bool _texturesLoaded { false }; + Transform _transform; + glm::vec3 _dimensions; + + bool _texturesLoaded { false }; + bool _retryApply { false }; + + std::shared_ptr getMaterial() const; + void setCurrentMaterialName(const std::string& currentMaterialName); + + void applyTextureTransform(std::shared_ptr& material); + void applyMaterial(); + void deleteMaterial(const QUuid& oldParentID, const QString& oldParentMaterialName); + + NetworkMaterialResourcePointer _networkMaterial; + NetworkMaterialResource::ParsedMaterials _parsedMaterials; + std::shared_ptr _appliedMaterial; + std::string _currentMaterialName; - std::shared_ptr _drawMaterial; }; } } diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index ae9fdf572a..03c50008a0 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -928,9 +928,9 @@ void RenderableModelEntityItem::setJointTranslationsSet(const QVector& tra _needsJointSimulation = true; } -void RenderableModelEntityItem::locationChanged(bool tellPhysics) { +void RenderableModelEntityItem::locationChanged(bool tellPhysics, bool tellChildren) { DETAILED_PERFORMANCE_TIMER("locationChanged"); - EntityItem::locationChanged(tellPhysics); + EntityItem::locationChanged(tellPhysics, tellChildren); auto model = getModel(); if (model && model->isLoaded()) { model->updateRenderItems(); @@ -1032,9 +1032,7 @@ void RenderableModelEntityItem::copyAnimationJointDataToModel() { }); if (changed) { - forEachChild([&](SpatiallyNestablePointer object) { - object->locationChanged(false); - }); + locationChanged(false, true); } } @@ -1079,7 +1077,7 @@ render::hifi::Tag ModelEntityRenderer::getTagMask() const { uint32_t ModelEntityRenderer::metaFetchMetaSubItems(ItemIDs& subItems) { if (_model) { - auto metaSubItems = _subRenderItemIDs; + auto metaSubItems = _model->fetchRenderItemIDs(); subItems.insert(subItems.end(), metaSubItems.begin(), metaSubItems.end()); return (uint32_t)metaSubItems.size(); } @@ -1321,11 +1319,8 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce if (!_hasModel) { if (model) { model->removeFromScene(scene, transaction); + entity->bumpAncestorChainRenderableVersion(); withWriteLock([&] { _model.reset(); }); - transaction.updateItem(getRenderItemID(), [](PayloadProxyInterface& data) { - auto entityRenderer = static_cast(&data); - entityRenderer->clearSubRenderItemIDs(); - }); emit DependencyManager::get()-> modelRemovedFromScene(entity->getEntityItemID(), NestableType::Entity, _model); } @@ -1442,12 +1437,7 @@ void ModelEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& sce render::Item::Status::Getters statusGetters; makeStatusGetters(entity, statusGetters); model->addToScene(scene, transaction, statusGetters); - - auto newRenderItemIDs{ model->fetchRenderItemIDs() }; - transaction.updateItem(getRenderItemID(), [newRenderItemIDs](PayloadProxyInterface& data) { - auto entityRenderer = static_cast(&data); - entityRenderer->setSubRenderItemIDs(newRenderItemIDs); - }); + entity->bumpAncestorChainRenderableVersion(); processMaterials(); } } diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index 9adff9ca01..2fd1041c5f 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -108,7 +108,7 @@ public: virtual void setJointTranslations(const QVector& translations) override; virtual void setJointTranslationsSet(const QVector& translationsSet) override; - virtual void locationChanged(bool tellPhysics = true) override; + virtual void locationChanged(bool tellPhysics = true, bool tellChildren = true) override; virtual int getJointIndex(const QString& name) const override; virtual QStringList getJointNames() const override; diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp index 64c05b576b..454e8b136a 100644 --- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp @@ -29,6 +29,13 @@ gpu::PipelinePointer PolyLineEntityRenderer::_glowPipeline = nullptr; static const QUrl DEFAULT_POLYLINE_TEXTURE = PathUtils::resourcesUrl("images/paintStroke.png"); +#if defined(USE_GLES) +static bool DISABLE_DEFERRED = true; +#else +static const QString RENDER_FORWARD{ "HIFI_RENDER_FORWARD" }; +static bool DISABLE_DEFERRED = QProcessEnvironment::systemEnvironment().contains(RENDER_FORWARD); +#endif + PolyLineEntityRenderer::PolyLineEntityRenderer(const EntityItemPointer& entity) : Parent(entity) { _texture = DependencyManager::get()->getTexture(DEFAULT_POLYLINE_TEXTURE); @@ -44,7 +51,13 @@ PolyLineEntityRenderer::PolyLineEntityRenderer(const EntityItemPointer& entity) void PolyLineEntityRenderer::buildPipeline() { // FIXME: opaque pipeline - gpu::ShaderPointer program = gpu::Shader::createProgram(shader::entities_renderer::program::paintStroke); + gpu::ShaderPointer program; + if (DISABLE_DEFERRED) { + program = gpu::Shader::createProgram(shader::entities_renderer::program::paintStroke_forward); + } else { + program = gpu::Shader::createProgram(shader::entities_renderer::program::paintStroke); + } + { gpu::StatePointer state = gpu::StatePointer(new gpu::State()); state->setCullMode(gpu::State::CullMode::CULL_NONE); @@ -170,18 +183,19 @@ void PolyLineEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPo void PolyLineEntityRenderer::updateGeometry() { int maxNumVertices = std::min(_points.length(), _normals.length()); - bool doesStrokeWidthVary = false; - if (_widths.size() >= 0) { + if (_widths.size() > 0) { + float prevWidth = _widths[0]; for (int i = 1; i < maxNumVertices; i++) { - float width = PolyLineEntityItem::DEFAULT_LINE_WIDTH; - if (i < _widths.length()) { - width = _widths[i]; - } - if (width != _widths[i - 1]) { + float width = i < _widths.length() ? _widths[i] : PolyLineEntityItem::DEFAULT_LINE_WIDTH; + if (width != prevWidth) { doesStrokeWidthVary = true; break; } + if (i > _widths.length() + 1) { + break; + } + prevWidth = width; } } @@ -193,12 +207,13 @@ void PolyLineEntityRenderer::updateGeometry() { std::vector vertices; vertices.reserve(maxNumVertices); + for (int i = 0; i < maxNumVertices; i++) { // Position glm::vec3 point = _points[i]; - // uCoord float width = i < _widths.size() ? _widths[i] : PolyLineEntityItem::DEFAULT_LINE_WIDTH; + if (i > 0) { // First uCoord is 0.0f if (!_isUVModeStretch) { accumulatedDistance += glm::distance(point, _points[i - 1]); diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index d42a766faa..b61bb2cbda 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -30,6 +30,13 @@ using namespace render::entities; // 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; +#if defined(USE_GLES) +static bool DISABLE_DEFERRED = true; +#else +static const QString RENDER_FORWARD{ "HIFI_RENDER_FORWARD" }; +static bool DISABLE_DEFERRED = QProcessEnvironment::systemEnvironment().contains(RENDER_FORWARD); +#endif + static_assert(shader::render_utils::program::simple != 0, "Validate simple program exists"); static_assert(shader::render_utils::program::simple_transparent != 0, "Validate simple transparent program exists"); @@ -53,7 +60,7 @@ bool ShapeEntityRenderer::needsRenderUpdate() const { } auto mat = _materials.find("0"); - if (mat != _materials.end() && (mat->second.needsUpdate() || mat->second.areTexturesLoading())) { + if (mat != _materials.end() && mat->second.shouldUpdate()) { return true; } @@ -188,7 +195,7 @@ bool ShapeEntityRenderer::useMaterialPipeline(const graphics::MultiMaterial& mat ShapeKey ShapeEntityRenderer::getShapeKey() { auto mat = _materials.find("0"); - if (mat != _materials.end() && (mat->second.needsUpdate() || mat->second.areTexturesLoading())) { + if (mat != _materials.end() && mat->second.shouldUpdate()) { RenderPipelines::updateMultiMaterial(mat->second); } @@ -276,7 +283,7 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) { // FIXME, support instanced multi-shape rendering using multidraw indirect outColor.a *= _isFading ? Interpolate::calculateFadeRatio(_fadeStartTime) : 1.0f; render::ShapePipelinePointer pipeline; - if (_renderLayer == RenderLayer::WORLD) { + if (_renderLayer == RenderLayer::WORLD && !DISABLE_DEFERRED) { pipeline = outColor.a < 1.0f ? geometryCache->getTransparentShapePipeline() : geometryCache->getOpaqueShapePipeline(); } else { pipeline = outColor.a < 1.0f ? geometryCache->getForwardTransparentShapePipeline() : geometryCache->getForwardOpaqueShapePipeline(); diff --git a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp index 99912e9d91..dfc9277bf0 100644 --- a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp @@ -181,7 +181,7 @@ void TextEntityRenderer::doRender(RenderArgs* args) { gpu::Batch& batch = *args->_batch; auto transformToTopLeft = modelTransform; - transformToTopLeft.setRotation(EntityItem::getBillboardRotation(transformToTopLeft.getTranslation(), transformToTopLeft.getRotation(), _billboardMode)); + transformToTopLeft.setRotation(EntityItem::getBillboardRotation(transformToTopLeft.getTranslation(), transformToTopLeft.getRotation(), _billboardMode, args->getViewFrustum().getPosition())); transformToTopLeft.postTranslate(dimensions * glm::vec3(-0.5f, 0.5f, 0.0f)); // Go to the top left transformToTopLeft.setScale(1.0f); // Use a scale of one so that the text is not deformed diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index bf7820fecd..3b615ba467 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -101,21 +101,15 @@ bool WebEntityRenderer::isTransparent() const { } bool WebEntityRenderer::needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const { - if (_contextPosition != entity->getWorldPosition()) { - return true; - } - - { - QSharedPointer webSurface; - withReadLock([&] { - webSurface = _webSurface; - }); - if (webSurface && uvec2(getWindowSize(entity)) != toGlm(webSurface->size())) { + if (resultWithReadLock([&] { + if (_webSurface && uvec2(getWindowSize(entity)) != toGlm(_webSurface->size())) { + return true; + } + + if (_contextPosition != entity->getWorldPosition()) { return true; } - } - if(resultWithReadLock([&] { if (_color != entity->getColor()) { return true; } @@ -194,7 +188,7 @@ void WebEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene auto newContentType = getContentType(newSourceURL); ContentType currentContentType; withReadLock([&] { - urlChanged = _sourceURL != newSourceURL; + urlChanged = newSourceURL.isEmpty() || newSourceURL != _tryingToBuildURL; }); currentContentType = _contentType; @@ -206,7 +200,6 @@ void WebEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene } } - withWriteLock([&] { _inputMode = entity->getInputMode(); _dpi = entity->getDPI(); @@ -216,6 +209,8 @@ void WebEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene _billboardMode = entity->getBillboardMode(); if (_contentType == ContentType::NoContent) { + _tryingToBuildURL = newSourceURL; + _sourceURL = newSourceURL; return; } @@ -226,10 +221,12 @@ void WebEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene if (_webSurface) { if (_webSurface->getRootItem()) { - if (_contentType == ContentType::HtmlContent && urlChanged) { + if (_contentType == ContentType::HtmlContent && _sourceURL != newSourceURL) { _webSurface->getRootItem()->setProperty(URL_PROPERTY, newSourceURL); + _sourceURL = newSourceURL; + } else if (_contentType != ContentType::HtmlContent) { + _sourceURL = newSourceURL; } - _sourceURL = newSourceURL; { auto scriptURL = entity->getScriptURL(); @@ -269,6 +266,7 @@ void WebEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene _webSurface->resize(QSize(windowSize.x, windowSize.y)); updateModelTransformAndBound(); _renderTransform = getModelTransform(); + _renderTransform.setScale(1.0f); _renderTransform.postScale(entity->getScaledDimensions()); }); }); @@ -294,20 +292,21 @@ void WebEntityRenderer::doRender(RenderArgs* args) { }); // Try to update the texture - { - QSharedPointer webSurface; - withReadLock([&] { - webSurface = _webSurface; - }); - if (!webSurface) { - return; + OffscreenQmlSurface::TextureAndFence newTextureAndFence; + bool newTextureAvailable = false; + if (!resultWithReadLock([&] { + if (!_webSurface) { + return false; } - OffscreenQmlSurface::TextureAndFence newTextureAndFence; - bool newTextureAvailable = webSurface->fetchTexture(newTextureAndFence); - if (newTextureAvailable) { - _texture->setExternalTexture(newTextureAndFence.first, newTextureAndFence.second); - } + newTextureAvailable = _webSurface->fetchTexture(newTextureAndFence); + return true; + })) { + return; + } + + if (newTextureAvailable) { + _texture->setExternalTexture(newTextureAndFence.first, newTextureAndFence.second); } static const glm::vec2 texMin(0.0f), texMax(1.0f), topLeft(-0.5f), bottomRight(0.5f); @@ -323,7 +322,7 @@ void WebEntityRenderer::doRender(RenderArgs* args) { }); batch.setResourceTexture(0, _texture); - transform.setRotation(EntityItem::getBillboardRotation(transform.getTranslation(), transform.getRotation(), _billboardMode)); + transform.setRotation(EntityItem::getBillboardRotation(transform.getTranslation(), transform.getRotation(), _billboardMode, args->getViewFrustum().getPosition())); batch.setModelTransform(transform); // Turn off jitter for these entities @@ -351,6 +350,8 @@ void WebEntityRenderer::buildWebSurface(const EntityItemPointer& entity, const Q _connections.push_back(QObject::connect(_webSurface.data(), &OffscreenQmlSurface::webEventReceived, this, [entityItemID](const QVariant& message) { emit DependencyManager::get()->webEventReceived(entityItemID, message); })); + + _tryingToBuildURL = newSourceURL; } void WebEntityRenderer::destroyWebSurface() { @@ -383,11 +384,16 @@ glm::vec2 WebEntityRenderer::getWindowSize(const TypedEntityPointer& entity) con void WebEntityRenderer::hoverEnterEntity(const PointerEvent& event) { if (_inputMode == WebInputMode::MOUSE) { handlePointerEvent(event); - } else if (_webSurface) { - PointerEvent webEvent = event; - webEvent.setPos2D(event.getPos2D() * (METERS_TO_INCHES * _dpi)); - _webSurface->hoverBeginEvent(webEvent, _touchDevice); + return; } + + withReadLock([&] { + if (_webSurface) { + PointerEvent webEvent = event; + webEvent.setPos2D(event.getPos2D() * (METERS_TO_INCHES * _dpi)); + _webSurface->hoverBeginEvent(webEvent, _touchDevice); + } + }); } void WebEntityRenderer::hoverLeaveEntity(const PointerEvent& event) { @@ -398,34 +404,39 @@ void WebEntityRenderer::hoverLeaveEntity(const PointerEvent& event) { // QML onReleased is only triggered if a click has happened first. We need to send this "fake" mouse move event to properly trigger an onExited. PointerEvent endMoveEvent(PointerEvent::Move, event.getID()); handlePointerEvent(endMoveEvent); - } else if (_webSurface) { - PointerEvent webEvent = event; - webEvent.setPos2D(event.getPos2D() * (METERS_TO_INCHES * _dpi)); - _webSurface->hoverEndEvent(webEvent, _touchDevice); - } -} - -void WebEntityRenderer::handlePointerEvent(const PointerEvent& event) { - if (_inputMode == WebInputMode::TOUCH) { - handlePointerEventAsTouch(event); - } else { - handlePointerEventAsMouse(event); - } -} - -void WebEntityRenderer::handlePointerEventAsTouch(const PointerEvent& event) { - if (_webSurface) { - PointerEvent webEvent = event; - webEvent.setPos2D(event.getPos2D() * (METERS_TO_INCHES * _dpi)); - _webSurface->handlePointerEvent(webEvent, _touchDevice); - } -} - -void WebEntityRenderer::handlePointerEventAsMouse(const PointerEvent& event) { - if (!_webSurface) { return; } + withReadLock([&] { + if (_webSurface) { + PointerEvent webEvent = event; + webEvent.setPos2D(event.getPos2D() * (METERS_TO_INCHES * _dpi)); + _webSurface->hoverEndEvent(webEvent, _touchDevice); + } + }); +} + +void WebEntityRenderer::handlePointerEvent(const PointerEvent& event) { + withReadLock([&] { + if (!_webSurface) { + return; + } + + if (_inputMode == WebInputMode::TOUCH) { + handlePointerEventAsTouch(event); + } else { + handlePointerEventAsMouse(event); + } + }); +} + +void WebEntityRenderer::handlePointerEventAsTouch(const PointerEvent& event) { + PointerEvent webEvent = event; + webEvent.setPos2D(event.getPos2D() * (METERS_TO_INCHES * _dpi)); + _webSurface->handlePointerEvent(webEvent, _touchDevice); +} + +void WebEntityRenderer::handlePointerEventAsMouse(const PointerEvent& event) { glm::vec2 windowPos = event.getPos2D() * (METERS_TO_INCHES * _dpi); QPointF windowPoint(windowPos.x, windowPos.y); @@ -459,18 +470,22 @@ void WebEntityRenderer::handlePointerEventAsMouse(const PointerEvent& event) { } void WebEntityRenderer::setProxyWindow(QWindow* proxyWindow) { - if (_webSurface) { - _webSurface->setProxyWindow(proxyWindow); - } + withReadLock([&] { + if (_webSurface) { + _webSurface->setProxyWindow(proxyWindow); + } + }); } QObject* WebEntityRenderer::getEventHandler() { - if (!_webSurface) { - return nullptr; - } - return _webSurface->getEventHandler(); + return resultWithReadLock([&]() -> QObject* { + if (!_webSurface) { + return nullptr; + } + return _webSurface->getEventHandler(); + }); } void WebEntityRenderer::emitScriptEvent(const QVariant& message) { emit scriptEventReceived(message); -} \ No newline at end of file +} diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.h b/libraries/entities-renderer/src/RenderableWebEntityItem.h index 30b63a72df..0345898b62 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.h +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.h @@ -82,6 +82,7 @@ private: QSharedPointer _webSurface { nullptr }; bool _cachedWebSurface { false }; gpu::TexturePointer _texture; + QString _tryingToBuildURL; glm::u8vec3 _color; float _alpha { 1.0f }; diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.h b/libraries/entities-renderer/src/RenderableZoneEntityItem.h index 32b5cf94a0..2fa55f1540 100644 --- a/libraries/entities-renderer/src/RenderableZoneEntityItem.h +++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.h @@ -150,7 +150,7 @@ public: virtual bool addToScene(const EntityItemPointer& self, const render::ScenePointer& scene, render::Transaction& transaction) override; virtual void removeFromScene(const EntityItemPointer& self, const render::ScenePointer& scene, render::Transaction& transaction) override; private: - virtual void locationChanged(bool tellPhysics = true) override { EntityItem::locationChanged(tellPhysics); notifyBoundChanged(); } + virtual void locationChanged(bool tellPhysics = true, bool tellChildren = true) override { EntityItem::locationChanged(tellPhysics, tellChildren); notifyBoundChanged(); } virtual void dimensionsChanged() override { EntityItem::dimensionsChanged(); notifyBoundChanged(); } void notifyBoundChanged(); void notifyChangedRenderItem(); diff --git a/libraries/entities-renderer/src/entities-renderer/paintStroke_forward.slp b/libraries/entities-renderer/src/entities-renderer/paintStroke_forward.slp new file mode 100644 index 0000000000..4d49e0d3a4 --- /dev/null +++ b/libraries/entities-renderer/src/entities-renderer/paintStroke_forward.slp @@ -0,0 +1 @@ +VERTEX paintStroke \ No newline at end of file diff --git a/libraries/entities-renderer/src/paintStroke_forward.slf b/libraries/entities-renderer/src/paintStroke_forward.slf new file mode 100644 index 0000000000..b949332826 --- /dev/null +++ b/libraries/entities-renderer/src/paintStroke_forward.slf @@ -0,0 +1,35 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// paintStroke.frag +// fragment shader +// +// Created by Eric Levin on 8/10/2015 +// 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 paintStroke.slh@> +<$declarePolyLineBuffers()$> + +LAYOUT(binding=0) uniform sampler2D _texture; + +layout(location=0) in vec3 _normalWS; +layout(location=1) in vec2 _texCoord; +layout(location=2) in vec4 _color; +layout(location=3) in float _distanceFromCenter; +layout(location=0) out vec4 _fragColor0; + +void main(void) { + vec4 texel = texture(_texture, _texCoord); + int frontCondition = 1 - 2 * int(gl_FrontFacing); + vec3 color = _color.rgb * texel.rgb; + float alpha = texel.a * _color.a; + + alpha *= mix(1.0, pow(1.0 - abs(_distanceFromCenter), 10.0), _polylineData.faceCameraGlow.y); + + _fragColor0 = vec4(color, alpha); +} diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 2c6d679b46..40bba5a0df 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -41,6 +41,7 @@ #include "EntitySimulation.h" #include "EntityDynamicFactoryInterface.h" +//#define WANT_DEBUG Q_DECLARE_METATYPE(EntityItemPointer); int entityItemPointernMetaTypeId = qRegisterMetaType(); @@ -49,7 +50,8 @@ int EntityItem::_maxActionsDataSize = 800; quint64 EntityItem::_rememberDeletedActionTime = 20 * USECS_PER_SECOND; QString EntityItem::_marketplacePublicKey; -std::function EntityItem::_getBillboardRotationOperator = [](const glm::vec3&, const glm::quat& rotation, BillboardMode) { return rotation; }; +std::function EntityItem::_getBillboardRotationOperator = [](const glm::vec3&, const glm::quat& rotation, BillboardMode, const glm::vec3&) { return rotation; }; +std::function EntityItem::_getPrimaryViewFrustumPositionOperator = []() { return glm::vec3(0.0f); }; EntityItem::EntityItem(const EntityItemID& entityItemID) : SpatiallyNestable(NestableType::Entity, entityItemID) @@ -78,6 +80,8 @@ EntityPropertyFlags EntityItem::getEntityProperties(EncodeBitstreamParams& param // Core requestedProperties += PROP_SIMULATION_OWNER; + requestedProperties += PROP_PARENT_ID; + requestedProperties += PROP_PARENT_JOINT_INDEX; requestedProperties += PROP_VISIBLE; requestedProperties += PROP_NAME; requestedProperties += PROP_LOCKED; @@ -259,20 +263,6 @@ OctreeElement::AppendState EntityItem::appendEntityData(OctreePacketData* packet // PROP_CUSTOM_PROPERTIES_INCLUDED, APPEND_ENTITY_PROPERTY(PROP_SIMULATION_OWNER, _simulationOwner.toByteArray()); - APPEND_ENTITY_PROPERTY(PROP_VISIBLE, getVisible()); - APPEND_ENTITY_PROPERTY(PROP_NAME, getName()); - APPEND_ENTITY_PROPERTY(PROP_LOCKED, getLocked()); - APPEND_ENTITY_PROPERTY(PROP_USER_DATA, getUserData()); - APPEND_ENTITY_PROPERTY(PROP_HREF, getHref()); - APPEND_ENTITY_PROPERTY(PROP_DESCRIPTION, getDescription()); - APPEND_ENTITY_PROPERTY(PROP_POSITION, getLocalPosition()); - APPEND_ENTITY_PROPERTY(PROP_DIMENSIONS, getUnscaledDimensions()); - APPEND_ENTITY_PROPERTY(PROP_ROTATION, getLocalOrientation()); - APPEND_ENTITY_PROPERTY(PROP_REGISTRATION_POINT, getRegistrationPoint()); - APPEND_ENTITY_PROPERTY(PROP_CREATED, getCreated()); - APPEND_ENTITY_PROPERTY(PROP_LAST_EDITED_BY, getLastEditedBy()); - // APPEND_ENTITY_PROPERTY(PROP_ENTITY_HOST_TYPE, (uint32_t)getEntityHostType()); // not sent over the wire - // APPEND_ENTITY_PROPERTY(PROP_OWNING_AVATAR_ID, getOwningAvatarID()); // not sent over the wire // convert AVATAR_SELF_ID to actual sessionUUID. QUuid actualParentID = getParentID(); if (actualParentID == AVATAR_SELF_ID) { @@ -281,6 +271,20 @@ OctreeElement::AppendState EntityItem::appendEntityData(OctreePacketData* packet } APPEND_ENTITY_PROPERTY(PROP_PARENT_ID, actualParentID); APPEND_ENTITY_PROPERTY(PROP_PARENT_JOINT_INDEX, getParentJointIndex()); + APPEND_ENTITY_PROPERTY(PROP_VISIBLE, getVisible()); + APPEND_ENTITY_PROPERTY(PROP_NAME, getName()); + APPEND_ENTITY_PROPERTY(PROP_LOCKED, getLocked()); + APPEND_ENTITY_PROPERTY(PROP_USER_DATA, getUserData()); + APPEND_ENTITY_PROPERTY(PROP_HREF, getHref()); + APPEND_ENTITY_PROPERTY(PROP_DESCRIPTION, getDescription()); + APPEND_ENTITY_PROPERTY(PROP_POSITION, getLocalPosition()); + APPEND_ENTITY_PROPERTY(PROP_DIMENSIONS, getScaledDimensions()); + APPEND_ENTITY_PROPERTY(PROP_ROTATION, getLocalOrientation()); + APPEND_ENTITY_PROPERTY(PROP_REGISTRATION_POINT, getRegistrationPoint()); + APPEND_ENTITY_PROPERTY(PROP_CREATED, getCreated()); + APPEND_ENTITY_PROPERTY(PROP_LAST_EDITED_BY, getLastEditedBy()); + // APPEND_ENTITY_PROPERTY(PROP_ENTITY_HOST_TYPE, (uint32_t)getEntityHostType()); // not sent over the wire + // APPEND_ENTITY_PROPERTY(PROP_OWNING_AVATAR_ID, getOwningAvatarID()); // not sent over the wire APPEND_ENTITY_PROPERTY(PROP_QUERY_AA_CUBE, getQueryAACube()); APPEND_ENTITY_PROPERTY(PROP_CAN_CAST_SHADOW, getCanCastShadow()); // APPEND_ENTITY_PROPERTY(PROP_VISIBLE_IN_SECONDARY_CAMERA, getIsVisibleInSecondaryCamera()); // not sent over the wire @@ -501,6 +505,7 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef } #ifdef WANT_DEBUG + { quint64 lastEdited = getLastEdited(); float editedAgo = getEditedAgo(); QString agoAsString = formatSecondsElapsed(editedAgo); @@ -514,6 +519,7 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef qCDebug(entities) << " age=" << getAge() << "seconds - " << ageAsString; qCDebug(entities) << " lastEdited =" << lastEdited; qCDebug(entities) << " ago=" << editedAgo << "seconds - " << agoAsString; + } #endif quint64 lastEditedFromBuffer = 0; @@ -791,6 +797,13 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef // Core // PROP_SIMULATION_OWNER handled above + { // parentID and parentJointIndex are protected by simulation ownership + bool oldOverwrite = overwriteLocalData; + overwriteLocalData = overwriteLocalData && !weOwnSimulation; + READ_ENTITY_PROPERTY(PROP_PARENT_ID, QUuid, setParentID); + READ_ENTITY_PROPERTY(PROP_PARENT_JOINT_INDEX, quint16, setParentJointIndex); + overwriteLocalData = oldOverwrite; + } READ_ENTITY_PROPERTY(PROP_VISIBLE, bool, setVisible); READ_ENTITY_PROPERTY(PROP_NAME, QString, setName); READ_ENTITY_PROPERTY(PROP_LOCKED, bool, setLocked); @@ -818,7 +831,7 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef }; READ_ENTITY_PROPERTY(PROP_POSITION, glm::vec3, customUpdatePositionFromNetwork); } - READ_ENTITY_PROPERTY(PROP_DIMENSIONS, glm::vec3, setUnscaledDimensions); + READ_ENTITY_PROPERTY(PROP_DIMENSIONS, glm::vec3, setScaledDimensions); { // See comment above auto customUpdateRotationFromNetwork = [this, shouldUpdate, lastEdited](glm::quat value) { if (shouldUpdate(_lastUpdatedRotationTimestamp, value != _lastUpdatedRotationValue)) { @@ -834,13 +847,6 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef READ_ENTITY_PROPERTY(PROP_LAST_EDITED_BY, QUuid, setLastEditedBy); // READ_ENTITY_PROPERTY(PROP_ENTITY_HOST_TYPE, entity::HostType, setEntityHostType); // not sent over the wire // READ_ENTITY_PROPERTY(PROP_OWNING_AVATAR_ID, QUuuid, setOwningAvatarID); // not sent over the wire - { // parentID and parentJointIndex are protected by simulation ownership - bool oldOverwrite = overwriteLocalData; - overwriteLocalData = overwriteLocalData && !weOwnSimulation; - READ_ENTITY_PROPERTY(PROP_PARENT_ID, QUuid, setParentID); - READ_ENTITY_PROPERTY(PROP_PARENT_JOINT_INDEX, quint16, setParentJointIndex); - overwriteLocalData = oldOverwrite; - } { // See comment above auto customUpdateQueryAACubeFromNetwork = [this, shouldUpdate, lastEdited](AACube value) { if (shouldUpdate(_lastUpdatedQueryAACubeTimestamp, value != _lastUpdatedQueryAACubeValue)) { @@ -1098,7 +1104,7 @@ void EntityItem::simulate(const quint64& now) { qCDebug(entities) << " hasGravity=" << hasGravity(); qCDebug(entities) << " hasAcceleration=" << hasAcceleration(); qCDebug(entities) << " hasAngularVelocity=" << hasAngularVelocity(); - qCDebug(entities) << " getAngularVelocity=" << getAngularVelocity(); + qCDebug(entities) << " getAngularVelocity=" << getLocalAngularVelocity(); qCDebug(entities) << " isMortal=" << isMortal(); qCDebug(entities) << " getAge()=" << getAge(); qCDebug(entities) << " getLifetime()=" << getLifetime(); @@ -1110,12 +1116,12 @@ void EntityItem::simulate(const quint64& now) { qCDebug(entities) << " hasGravity=" << hasGravity(); qCDebug(entities) << " hasAcceleration=" << hasAcceleration(); qCDebug(entities) << " hasAngularVelocity=" << hasAngularVelocity(); - qCDebug(entities) << " getAngularVelocity=" << getAngularVelocity(); + qCDebug(entities) << " getAngularVelocity=" << getLocalAngularVelocity(); } if (hasAngularVelocity()) { qCDebug(entities) << " CHANGING...="; qCDebug(entities) << " hasAngularVelocity=" << hasAngularVelocity(); - qCDebug(entities) << " getAngularVelocity=" << getAngularVelocity(); + qCDebug(entities) << " getAngularVelocity=" << getLocalAngularVelocity(); } if (isMortal()) { qCDebug(entities) << " MORTAL...="; @@ -1308,6 +1314,8 @@ EntityItemProperties EntityItem::getProperties(const EntityPropertyFlags& desire // Core COPY_ENTITY_PROPERTY_TO_PROPERTIES(simulationOwner, getSimulationOwner); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(parentID, getParentID); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(parentJointIndex, getParentJointIndex); COPY_ENTITY_PROPERTY_TO_PROPERTIES(visible, getVisible); COPY_ENTITY_PROPERTY_TO_PROPERTIES(name, getName); COPY_ENTITY_PROPERTY_TO_PROPERTIES(locked, getLocked); @@ -1315,15 +1323,13 @@ EntityItemProperties EntityItem::getProperties(const EntityPropertyFlags& desire COPY_ENTITY_PROPERTY_TO_PROPERTIES(href, getHref); COPY_ENTITY_PROPERTY_TO_PROPERTIES(description, getDescription); COPY_ENTITY_PROPERTY_TO_PROPERTIES(position, getLocalPosition); - COPY_ENTITY_PROPERTY_TO_PROPERTIES(dimensions, getUnscaledDimensions); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(dimensions, getScaledDimensions); COPY_ENTITY_PROPERTY_TO_PROPERTIES(rotation, getLocalOrientation); COPY_ENTITY_PROPERTY_TO_PROPERTIES(registrationPoint, getRegistrationPoint); COPY_ENTITY_PROPERTY_TO_PROPERTIES(created, getCreated); COPY_ENTITY_PROPERTY_TO_PROPERTIES(lastEditedBy, getLastEditedBy); COPY_ENTITY_PROPERTY_TO_PROPERTIES(entityHostType, getEntityHostType); COPY_ENTITY_PROPERTY_TO_PROPERTIES(owningAvatarID, getOwningAvatarID); - COPY_ENTITY_PROPERTY_TO_PROPERTIES(parentID, getParentID); - COPY_ENTITY_PROPERTY_TO_PROPERTIES(parentJointIndex, getParentJointIndex); COPY_ENTITY_PROPERTY_TO_PROPERTIES(queryAACube, getQueryAACube); COPY_ENTITY_PROPERTY_TO_PROPERTIES(canCastShadow, getCanCastShadow); COPY_ENTITY_PROPERTY_TO_PROPERTIES(isVisibleInSecondaryCamera, isVisibleInSecondaryCamera); @@ -1455,6 +1461,8 @@ bool EntityItem::setProperties(const EntityItemProperties& properties) { // Core SET_ENTITY_PROPERTY_FROM_PROPERTIES(simulationOwner, setSimulationOwner); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(parentID, setParentID); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(parentJointIndex, setParentJointIndex); SET_ENTITY_PROPERTY_FROM_PROPERTIES(visible, setVisible); SET_ENTITY_PROPERTY_FROM_PROPERTIES(name, setName); SET_ENTITY_PROPERTY_FROM_PROPERTIES(locked, setLocked); @@ -1462,15 +1470,13 @@ bool EntityItem::setProperties(const EntityItemProperties& properties) { SET_ENTITY_PROPERTY_FROM_PROPERTIES(href, setHref); SET_ENTITY_PROPERTY_FROM_PROPERTIES(description, setDescription); SET_ENTITY_PROPERTY_FROM_PROPERTIES(position, setPosition); - SET_ENTITY_PROPERTY_FROM_PROPERTIES(dimensions, setUnscaledDimensions); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(dimensions, setScaledDimensions); SET_ENTITY_PROPERTY_FROM_PROPERTIES(rotation, setRotation); SET_ENTITY_PROPERTY_FROM_PROPERTIES(registrationPoint, setRegistrationPoint); SET_ENTITY_PROPERTY_FROM_PROPERTIES(created, setCreated); SET_ENTITY_PROPERTY_FROM_PROPERTIES(lastEditedBy, setLastEditedBy); SET_ENTITY_PROPERTY_FROM_PROPERTIES(entityHostType, setEntityHostType); SET_ENTITY_PROPERTY_FROM_PROPERTIES(owningAvatarID, setOwningAvatarID); - SET_ENTITY_PROPERTY_FROM_PROPERTIES(parentID, setParentID); - SET_ENTITY_PROPERTY_FROM_PROPERTIES(parentJointIndex, setParentJointIndex); SET_ENTITY_PROPERTY_FROM_PROPERTIES(queryAACube, setQueryAACube); SET_ENTITY_PROPERTY_FROM_PROPERTIES(canCastShadow, setCanCastShadow); SET_ENTITY_PROPERTY_FROM_PROPERTIES(isVisibleInSecondaryCamera, setIsVisibleInSecondaryCamera); @@ -1737,7 +1743,7 @@ bool EntityItem::contains(const glm::vec3& point) const { // the above cases not yet supported --> fall through to BOX case case SHAPE_TYPE_BOX: { localPoint = glm::abs(localPoint); - return glm::any(glm::lessThanEqual(localPoint, glm::vec3(NORMALIZED_HALF_SIDE))); + return glm::all(glm::lessThanEqual(localPoint, glm::vec3(NORMALIZED_HALF_SIDE))); } case SHAPE_TYPE_ELLIPSOID: { // since we've transformed into the normalized space this is just a sphere-point intersection test @@ -1867,12 +1873,12 @@ void EntityItem::setParentID(const QUuid& value) { glm::vec3 EntityItem::getScaledDimensions() const { glm::vec3 scale = getSNScale(); - return _unscaledDimensions * scale; + return getUnscaledDimensions() * scale; } void EntityItem::setScaledDimensions(const glm::vec3& value) { glm::vec3 parentScale = getSNScale(); - setUnscaledDimensions(value * parentScale); + setUnscaledDimensions(value / parentScale); } void EntityItem::setUnscaledDimensions(const glm::vec3& value) { @@ -2587,7 +2593,7 @@ QList EntityItem::getActionsOfType(EntityDynamicType typeT return result; } -void EntityItem::locationChanged(bool tellPhysics) { +void EntityItem::locationChanged(bool tellPhysics, bool tellChildren) { requiresRecalcBoxes(); if (tellPhysics) { _flags |= Simulation::DIRTY_TRANSFORM; @@ -2596,7 +2602,7 @@ void EntityItem::locationChanged(bool tellPhysics) { tree->entityChanged(getThisPointer()); } } - SpatiallyNestable::locationChanged(tellPhysics); // tell all the children, also + SpatiallyNestable::locationChanged(tellPhysics, tellChildren); std::pair data(_spaceIndex, glm::vec4(getWorldPosition(), _boundingRadius)); emit spaceUpdate(data); somethingChangedNotification(); @@ -2651,13 +2657,23 @@ bool EntityItem::matchesJSONFilters(const QJsonObject& jsonFilters) const { // ALL entity properties. Some work will need to be done to the property system so that it can be more flexible // (to grab the value and default value of a property given the string representation of that property, for example) - // currently the only property filter we handle is '+' for serverScripts + // currently the only property filter we handle in EntityItem is '+' for serverScripts // which means that we only handle a filtered query asking for entities where the serverScripts property is non-default static const QString SERVER_SCRIPTS_PROPERTY = "serverScripts"; + static const QString ENTITY_TYPE_PROPERTY = "type"; - if (jsonFilters[SERVER_SCRIPTS_PROPERTY] == EntityQueryFilterSymbol::NonDefault) { - return _serverScripts != ENTITY_ITEM_DEFAULT_SERVER_SCRIPTS; + foreach(const auto& property, jsonFilters.keys()) { + if (property == SERVER_SCRIPTS_PROPERTY && jsonFilters[property] == EntityQueryFilterSymbol::NonDefault) { + // check if this entity has a non-default value for serverScripts + if (_serverScripts != ENTITY_ITEM_DEFAULT_SERVER_SCRIPTS) { + return true; + } else { + return false; + } + } else if (property == ENTITY_TYPE_PROPERTY) { + return (jsonFilters[property] == EntityTypes::getEntityTypeName(getType()) ); + } } // the json filter syntax did not match what we expected, return a match @@ -2935,6 +2951,7 @@ void EntityItem::setVisible(bool value) { }); if (changed) { + bumpAncestorChainRenderableVersion(); emit requestRenderUpdate(); } } @@ -3250,25 +3267,6 @@ void EntityItem::setSpaceIndex(int32_t index) { void EntityItem::preDelete() { } -void EntityItem::addMaterial(graphics::MaterialLayer material, const std::string& parentMaterialName) { - std::lock_guard lock(_materialsLock); - _materials[parentMaterialName].push(material); -} - -void EntityItem::removeMaterial(graphics::MaterialPointer material, const std::string& parentMaterialName) { - std::lock_guard lock(_materialsLock); - _materials[parentMaterialName].remove(material); -} - -std::unordered_map EntityItem::getMaterials() { - std::unordered_map toReturn; - { - std::lock_guard lock(_materialsLock); - toReturn = _materials; - } - return toReturn; -} - bool EntityItem::getCloneable() const { bool result; withReadLock([&] { diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 5d6627d461..f8c9c3b6f7 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -37,8 +37,6 @@ #include "EntityDynamicInterface.h" #include "GrabPropertyGroup.h" -#include "graphics/Material.h" - class EntitySimulation; class EntityTreeElement; class EntityTreeElementExtraEncodeData; @@ -199,7 +197,7 @@ public: void setDescription(const QString& value); /// Dimensions in meters (0.0 - TREE_SCALE) - glm::vec3 getScaledDimensions() const; + virtual glm::vec3 getScaledDimensions() const; virtual void setScaledDimensions(const glm::vec3& value); virtual glm::vec3 getRaycastDimensions() const { return getScaledDimensions(); } @@ -516,11 +514,11 @@ public: QUuid getLastEditedBy() const { return _lastEditedBy; } void setLastEditedBy(QUuid value) { _lastEditedBy = value; } - bool matchesJSONFilters(const QJsonObject& jsonFilters) const; + virtual bool matchesJSONFilters(const QJsonObject& jsonFilters) const; virtual bool getMeshes(MeshProxyList& result) { return true; } - virtual void locationChanged(bool tellPhysics = true) override; + virtual void locationChanged(bool tellPhysics = true, bool tellChildren = true) override; virtual bool getScalesWithParent() const override; @@ -542,10 +540,6 @@ public: virtual void preDelete(); virtual void postParentFixup() {} - void addMaterial(graphics::MaterialLayer material, const std::string& parentMaterialName); - void removeMaterial(graphics::MaterialPointer material, const std::string& parentMaterialName); - std::unordered_map getMaterials(); - void setSimulationOwnershipExpiry(uint64_t expiry) { _simulationOwnershipExpiry = expiry; } uint64_t getSimulationOwnershipExpiry() const { return _simulationOwnershipExpiry; } @@ -563,8 +557,10 @@ public: virtual void removeGrab(GrabPointer grab) override; virtual void disableGrab(GrabPointer grab) override; - static void setBillboardRotationOperator(std::function getBillboardRotationOperator) { _getBillboardRotationOperator = getBillboardRotationOperator; } - static glm::quat getBillboardRotation(const glm::vec3& position, const glm::quat& rotation, BillboardMode billboardMode) { return _getBillboardRotationOperator(position, rotation, billboardMode); } + static void setBillboardRotationOperator(std::function getBillboardRotationOperator) { _getBillboardRotationOperator = getBillboardRotationOperator; } + static glm::quat getBillboardRotation(const glm::vec3& position, const glm::quat& rotation, BillboardMode billboardMode, const glm::vec3& frustumPos) { return _getBillboardRotationOperator(position, rotation, billboardMode, frustumPos); } + static void setPrimaryViewFrustumPositionOperator(std::function getPrimaryViewFrustumPositionOperator) { _getPrimaryViewFrustumPositionOperator = getPrimaryViewFrustumPositionOperator; } + static glm::vec3 getPrimaryViewFrustumPosition() { return _getPrimaryViewFrustumPositionOperator(); } signals: void requestRenderUpdate(); @@ -754,11 +750,8 @@ protected: QHash _grabActions; private: - std::unordered_map _materials; - std::mutex _materialsLock; - - static std::function _getBillboardRotationOperator; - + static std::function _getBillboardRotationOperator; + static std::function _getPrimaryViewFrustumPositionOperator; }; #endif // hifi_EntityItem_h diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 6738b1cedd..80a6a90841 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -225,6 +225,15 @@ QString EntityItemProperties::getBloomModeAsString() const { return getComponentModeAsString(_bloomMode); } +namespace { + const QStringList AVATAR_PRIORITIES_AS_STRING + { "inherit", "crowd", "hero" }; +} + +QString EntityItemProperties::getAvatarPriorityAsString() const { + return AVATAR_PRIORITIES_AS_STRING.value(_avatarPriority); +} + std::array::const_iterator EntityItemProperties::findComponent(const QString& mode) { return std::find_if(COMPONENT_MODES.begin(), COMPONENT_MODES.end(), [&](const ComponentPair& pair) { return (pair.second == mode); @@ -249,6 +258,15 @@ void EntityItemProperties::setBloomModeFromString(const QString& bloomMode) { } } +void EntityItemProperties::setAvatarPriorityFromString(QString const& avatarPriority) { + auto result = AVATAR_PRIORITIES_AS_STRING.indexOf(avatarPriority); + + if (result != -1) { + _avatarPriority = result; + _avatarPriorityChanged = true; + } +} + QString EntityItemProperties::getKeyLightModeAsString() const { return getComponentModeAsString(_keyLightMode); } @@ -462,6 +480,8 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { // Core CHECK_PROPERTY_CHANGE(PROP_SIMULATION_OWNER, simulationOwner); + CHECK_PROPERTY_CHANGE(PROP_PARENT_ID, parentID); + CHECK_PROPERTY_CHANGE(PROP_PARENT_JOINT_INDEX, parentJointIndex); CHECK_PROPERTY_CHANGE(PROP_VISIBLE, visible); CHECK_PROPERTY_CHANGE(PROP_NAME, name); CHECK_PROPERTY_CHANGE(PROP_LOCKED, locked); @@ -476,8 +496,6 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_LAST_EDITED_BY, lastEditedBy); CHECK_PROPERTY_CHANGE(PROP_ENTITY_HOST_TYPE, entityHostType); CHECK_PROPERTY_CHANGE(PROP_OWNING_AVATAR_ID, owningAvatarID); - CHECK_PROPERTY_CHANGE(PROP_PARENT_ID, parentID); - CHECK_PROPERTY_CHANGE(PROP_PARENT_JOINT_INDEX, parentJointIndex); CHECK_PROPERTY_CHANGE(PROP_QUERY_AA_CUBE, queryAACube); CHECK_PROPERTY_CHANGE(PROP_CAN_CAST_SHADOW, canCastShadow); CHECK_PROPERTY_CHANGE(PROP_VISIBLE_IN_SECONDARY_CAMERA, isVisibleInSecondaryCamera); @@ -580,6 +598,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { // Model CHECK_PROPERTY_CHANGE(PROP_MODEL_URL, modelURL); + CHECK_PROPERTY_CHANGE(PROP_MODEL_SCALE, modelScale); CHECK_PROPERTY_CHANGE(PROP_JOINT_ROTATIONS_SET, jointRotationsSet); CHECK_PROPERTY_CHANGE(PROP_JOINT_ROTATIONS, jointRotations); CHECK_PROPERTY_CHANGE(PROP_JOINT_TRANSLATIONS_SET, jointTranslationsSet); @@ -621,6 +640,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_SKYBOX_MODE, skyboxMode); CHECK_PROPERTY_CHANGE(PROP_HAZE_MODE, hazeMode); CHECK_PROPERTY_CHANGE(PROP_BLOOM_MODE, bloomMode); + CHECK_PROPERTY_CHANGE(PROP_AVATAR_PRIORITY, avatarPriority); // Polyvox CHECK_PROPERTY_CHANGE(PROP_VOXEL_VOLUME_SIZE, voxelVolumeSize); @@ -1012,6 +1032,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { * @property {Vec3} dimensions=0.1,0.1,0.1 - The dimensions of the entity. When adding an entity, if no dimensions * value is specified then the model is automatically sized to its * {@link Entities.EntityProperties|naturalDimensions}. + * @property {Vec3} modelScale - The scale factor applied to the model's dimensions. Deprecated. * @property {Color} color=255,255,255 - Currently not used. * @property {string} modelURL="" - The URL of the FBX of OBJ model. Baked FBX models' URLs end in ".baked.fbx".
* @property {string} textures="" - A JSON string of texture name, URL pairs used when rendering the model in place of the @@ -1424,7 +1445,13 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { * @property {string} filterURL="" - The URL of a JavaScript file that filters changes to properties of entities within the * zone. It is periodically executed for each entity in the zone. It can, for example, be used to not allow changes to * certain properties.
+ * + * @property {string} avatarPriority="inherit" - Configures the update priority of contained avatars to other clients.
+ * "inherit": Priority from enclosing zones is unchanged.
+ * "crowd": Priority in this zone is the normal priority.
+ * "hero": Avatars in this zone will have an increased update priority *

+ *
  * function filter(properties) {
  *     // Test and edit properties object values,
  *     // e.g., properties.modelURL, as required.
@@ -1549,6 +1576,8 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool
 
     // Core properties
     //COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_SIMULATION_OWNER, simulationOwner); // not exposed yet
+    COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_PARENT_ID, parentID);
+    COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_PARENT_JOINT_INDEX, parentJointIndex);
     COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_VISIBLE, visible);
     COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_NAME, name);
     COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LOCKED, locked);
@@ -1563,8 +1592,6 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool
     COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LAST_EDITED_BY, lastEditedBy);
     COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_ENTITY_HOST_TYPE, entityHostType, getEntityHostTypeAsString());
     COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_OWNING_AVATAR_ID, owningAvatarID);
-    COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_PARENT_ID, parentID);
-    COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_PARENT_JOINT_INDEX, parentJointIndex);
     COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_QUERY_AA_CUBE, queryAACube);
     COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CAN_CAST_SHADOW, canCastShadow);
     COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_VISIBLE_IN_SECONDARY_CAMERA, isVisibleInSecondaryCamera);
@@ -1683,6 +1710,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_TEXTURES, textures);
 
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_MODEL_URL, modelURL);
+        COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_MODEL_SCALE, modelScale);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_JOINT_ROTATIONS_SET, jointRotationsSet);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_JOINT_ROTATIONS, jointRotations);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_JOINT_TRANSLATIONS_SET, jointTranslationsSet);
@@ -1758,6 +1786,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool
         COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_SKYBOX_MODE, skyboxMode, getSkyboxModeAsString());
         COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_HAZE_MODE, hazeMode, getHazeModeAsString());
         COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_BLOOM_MODE, bloomMode, getBloomModeAsString());
+        COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_AVATAR_PRIORITY, avatarPriority, getAvatarPriorityAsString());
     }
 
     // Web only
@@ -1953,6 +1982,8 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool
         // not handled yet
         // COPY_PROPERTY_FROM_QSCRIPTVALUE(simulationOwner, SimulationOwner, setSimulationOwner);
     }
+    COPY_PROPERTY_FROM_QSCRIPTVALUE(parentID, QUuid, setParentID);
+    COPY_PROPERTY_FROM_QSCRIPTVALUE(parentJointIndex, quint16, setParentJointIndex);
     COPY_PROPERTY_FROM_QSCRIPTVALUE(visible, bool, setVisible);
     COPY_PROPERTY_FROM_QSCRIPTVALUE(name, QString, setName);
     COPY_PROPERTY_FROM_QSCRIPTVALUE(locked, bool, setLocked);
@@ -1969,8 +2000,6 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool
         COPY_PROPERTY_FROM_QSCRIPTVALUE_ENUM(entityHostType, EntityHostType);
         COPY_PROPERTY_FROM_QSCRIPTVALUE(owningAvatarID, QUuid, setOwningAvatarID);
     }
-    COPY_PROPERTY_FROM_QSCRIPTVALUE(parentID, QUuid, setParentID);
-    COPY_PROPERTY_FROM_QSCRIPTVALUE(parentJointIndex, quint16, setParentJointIndex);
     COPY_PROPERTY_FROM_QSCRIPTVALUE(queryAACube, AACube, setQueryAACube); // TODO: should scripts be able to set this?
     COPY_PROPERTY_FROM_QSCRIPTVALUE(canCastShadow, bool, setCanCastShadow);
     COPY_PROPERTY_FROM_QSCRIPTVALUE(isVisibleInSecondaryCamera, bool, setIsVisibleInSecondaryCamera);
@@ -2078,6 +2107,7 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool
 
     // Model
     COPY_PROPERTY_FROM_QSCRIPTVALUE(modelURL, QString, setModelURL);
+    COPY_PROPERTY_FROM_QSCRIPTVALUE(modelScale, vec3, setModelScale);
     COPY_PROPERTY_FROM_QSCRIPTVALUE(jointRotationsSet, qVectorBool, setJointRotationsSet);
     COPY_PROPERTY_FROM_QSCRIPTVALUE(jointRotations, qVectorQuat, setJointRotations);
     COPY_PROPERTY_FROM_QSCRIPTVALUE(jointTranslationsSet, qVectorBool, setJointTranslationsSet);
@@ -2119,6 +2149,7 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool
     COPY_PROPERTY_FROM_QSCRIPTVALUE_ENUM(skyboxMode, SkyboxMode);
     COPY_PROPERTY_FROM_QSCRIPTVALUE_ENUM(hazeMode, HazeMode);
     COPY_PROPERTY_FROM_QSCRIPTVALUE_ENUM(bloomMode, BloomMode);
+    COPY_PROPERTY_FROM_QSCRIPTVALUE_ENUM(avatarPriority, AvatarPriority);
 
     // Polyvox
     COPY_PROPERTY_FROM_QSCRIPTVALUE(voxelVolumeSize, vec3, setVoxelVolumeSize);
@@ -2239,6 +2270,8 @@ void EntityItemProperties::copyFromJSONString(QScriptEngine& scriptEngine, const
 void EntityItemProperties::merge(const EntityItemProperties& other) {
     // Core
     COPY_PROPERTY_IF_CHANGED(simulationOwner);
+    COPY_PROPERTY_IF_CHANGED(parentID);
+    COPY_PROPERTY_IF_CHANGED(parentJointIndex);
     COPY_PROPERTY_IF_CHANGED(visible);
     COPY_PROPERTY_IF_CHANGED(name);
     COPY_PROPERTY_IF_CHANGED(locked);
@@ -2253,8 +2286,6 @@ void EntityItemProperties::merge(const EntityItemProperties& other) {
     COPY_PROPERTY_IF_CHANGED(lastEditedBy);
     COPY_PROPERTY_IF_CHANGED(entityHostType);
     COPY_PROPERTY_IF_CHANGED(owningAvatarID);
-    COPY_PROPERTY_IF_CHANGED(parentID);
-    COPY_PROPERTY_IF_CHANGED(parentJointIndex);
     COPY_PROPERTY_IF_CHANGED(queryAACube);
     COPY_PROPERTY_IF_CHANGED(canCastShadow);
     COPY_PROPERTY_IF_CHANGED(isVisibleInSecondaryCamera);
@@ -2357,6 +2388,7 @@ void EntityItemProperties::merge(const EntityItemProperties& other) {
 
     // Model
     COPY_PROPERTY_IF_CHANGED(modelURL);
+    COPY_PROPERTY_IF_CHANGED(modelScale);
     COPY_PROPERTY_IF_CHANGED(jointRotationsSet);
     COPY_PROPERTY_IF_CHANGED(jointRotations);
     COPY_PROPERTY_IF_CHANGED(jointTranslationsSet);
@@ -2398,6 +2430,7 @@ void EntityItemProperties::merge(const EntityItemProperties& other) {
     COPY_PROPERTY_IF_CHANGED(skyboxMode);
     COPY_PROPERTY_IF_CHANGED(hazeMode);
     COPY_PROPERTY_IF_CHANGED(bloomMode);
+    COPY_PROPERTY_IF_CHANGED(avatarPriority);
 
     // Polyvox
     COPY_PROPERTY_IF_CHANGED(voxelVolumeSize);
@@ -2521,6 +2554,8 @@ bool EntityItemProperties::getPropertyInfo(const QString& propertyName, EntityPr
     std::call_once(initMap, []() {
         // Core
         ADD_PROPERTY_TO_MAP(PROP_SIMULATION_OWNER, SimulationOwner, simulationOwner, SimulationOwner);
+        ADD_PROPERTY_TO_MAP(PROP_PARENT_ID, ParentID, parentID, QUuid);
+        ADD_PROPERTY_TO_MAP(PROP_PARENT_JOINT_INDEX, ParentJointIndex, parentJointIndex, uint16_t);
         ADD_PROPERTY_TO_MAP(PROP_VISIBLE, Visible, visible, bool);
         ADD_PROPERTY_TO_MAP(PROP_NAME, Name, name, QString);
         ADD_PROPERTY_TO_MAP(PROP_LOCKED, Locked, locked, bool);
@@ -2536,8 +2571,6 @@ bool EntityItemProperties::getPropertyInfo(const QString& propertyName, EntityPr
         ADD_PROPERTY_TO_MAP(PROP_LAST_EDITED_BY, LastEditedBy, lastEditedBy, QUuid);
         ADD_PROPERTY_TO_MAP(PROP_ENTITY_HOST_TYPE, EntityHostType, entityHostType, entity::HostType);
         ADD_PROPERTY_TO_MAP(PROP_OWNING_AVATAR_ID, OwningAvatarID, owningAvatarID, QUuid);
-        ADD_PROPERTY_TO_MAP(PROP_PARENT_ID, ParentID, parentID, QUuid);
-        ADD_PROPERTY_TO_MAP(PROP_PARENT_JOINT_INDEX, ParentJointIndex, parentJointIndex, uint16_t);
         ADD_PROPERTY_TO_MAP(PROP_QUERY_AA_CUBE, QueryAACube, queryAACube, AACube);
         ADD_PROPERTY_TO_MAP(PROP_CAN_CAST_SHADOW, CanCastShadow, canCastShadow, bool);
         ADD_PROPERTY_TO_MAP(PROP_VISIBLE_IN_SECONDARY_CAMERA, IsVisibleInSecondaryCamera, isVisibleInSecondaryCamera, bool);
@@ -2700,6 +2733,7 @@ bool EntityItemProperties::getPropertyInfo(const QString& propertyName, EntityPr
 
         // Model
         ADD_PROPERTY_TO_MAP(PROP_MODEL_URL, ModelURL, modelURL, QString);
+        ADD_PROPERTY_TO_MAP(PROP_MODEL_SCALE, ModelScale, modelScale, vec3);
         ADD_PROPERTY_TO_MAP(PROP_JOINT_ROTATIONS_SET, JointRotationsSet, jointRotationsSet, QVector);
         ADD_PROPERTY_TO_MAP(PROP_JOINT_ROTATIONS, JointRotations, jointRotations, QVector);
         ADD_PROPERTY_TO_MAP(PROP_JOINT_TRANSLATIONS_SET, JointTranslationsSet, jointTranslationsSet, QVector);
@@ -2783,6 +2817,7 @@ bool EntityItemProperties::getPropertyInfo(const QString& propertyName, EntityPr
         ADD_PROPERTY_TO_MAP(PROP_SKYBOX_MODE, SkyboxMode, skyboxMode, uint32_t);
         ADD_PROPERTY_TO_MAP(PROP_HAZE_MODE, HazeMode, hazeMode, uint32_t);
         ADD_PROPERTY_TO_MAP(PROP_BLOOM_MODE, BloomMode, bloomMode, uint32_t);
+        ADD_PROPERTY_TO_MAP(PROP_AVATAR_PRIORITY, AvatarPriority, avatarPriority, uint32_t);
 
         // Polyvox
         ADD_PROPERTY_TO_MAP(PROP_VOXEL_VOLUME_SIZE, VoxelVolumeSize, voxelVolumeSize, vec3);
@@ -2992,6 +3027,8 @@ OctreeElement::AppendState EntityItemProperties::encodeEntityEditPacket(PacketTy
 
 
             APPEND_ENTITY_PROPERTY(PROP_SIMULATION_OWNER, properties._simulationOwner.toByteArray());
+            APPEND_ENTITY_PROPERTY(PROP_PARENT_ID, properties.getParentID());
+            APPEND_ENTITY_PROPERTY(PROP_PARENT_JOINT_INDEX, properties.getParentJointIndex());
             APPEND_ENTITY_PROPERTY(PROP_VISIBLE, properties.getVisible());
             APPEND_ENTITY_PROPERTY(PROP_NAME, properties.getName());
             APPEND_ENTITY_PROPERTY(PROP_LOCKED, properties.getLocked());
@@ -3006,8 +3043,6 @@ OctreeElement::AppendState EntityItemProperties::encodeEntityEditPacket(PacketTy
             APPEND_ENTITY_PROPERTY(PROP_LAST_EDITED_BY, properties.getLastEditedBy());
             // APPEND_ENTITY_PROPERTY(PROP_ENTITY_HOST_TYPE, (uint32_t)properties.getEntityHostType());              // not sent over the wire
             // APPEND_ENTITY_PROPERTY(PROP_OWNING_AVATAR_ID, properties.getOwningAvatarID());                        // not sent over the wire
-            APPEND_ENTITY_PROPERTY(PROP_PARENT_ID, properties.getParentID());
-            APPEND_ENTITY_PROPERTY(PROP_PARENT_JOINT_INDEX, properties.getParentJointIndex());
             APPEND_ENTITY_PROPERTY(PROP_QUERY_AA_CUBE, properties.getQueryAACube());
             APPEND_ENTITY_PROPERTY(PROP_CAN_CAST_SHADOW, properties.getCanCastShadow());
             // APPEND_ENTITY_PROPERTY(PROP_VISIBLE_IN_SECONDARY_CAMERA, properties.getIsVisibleInSecondaryCamera()); // not sent over the wire
@@ -3118,6 +3153,7 @@ OctreeElement::AppendState EntityItemProperties::encodeEntityEditPacket(PacketTy
                 APPEND_ENTITY_PROPERTY(PROP_TEXTURES, properties.getTextures());
 
                 APPEND_ENTITY_PROPERTY(PROP_MODEL_URL, properties.getModelURL());
+                APPEND_ENTITY_PROPERTY(PROP_MODEL_SCALE, properties.getModelScale());
                 APPEND_ENTITY_PROPERTY(PROP_JOINT_ROTATIONS_SET, properties.getJointRotationsSet());
                 APPEND_ENTITY_PROPERTY(PROP_JOINT_ROTATIONS, properties.getJointRotations());
                 APPEND_ENTITY_PROPERTY(PROP_JOINT_TRANSLATIONS_SET, properties.getJointTranslationsSet());
@@ -3184,6 +3220,7 @@ OctreeElement::AppendState EntityItemProperties::encodeEntityEditPacket(PacketTy
                 APPEND_ENTITY_PROPERTY(PROP_SKYBOX_MODE, (uint32_t)properties.getSkyboxMode());
                 APPEND_ENTITY_PROPERTY(PROP_HAZE_MODE, (uint32_t)properties.getHazeMode());
                 APPEND_ENTITY_PROPERTY(PROP_BLOOM_MODE, (uint32_t)properties.getBloomMode());
+                APPEND_ENTITY_PROPERTY(PROP_AVATAR_PRIORITY, (uint32_t)properties.getAvatarPriority());
             }
 
             if (properties.getType() == EntityTypes::PolyVox) {
@@ -3471,6 +3508,8 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int
     processedBytes += propertyFlags.getEncodedLength();
 
     READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_SIMULATION_OWNER, QByteArray, setSimulationOwner);
+    READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_PARENT_ID, QUuid, setParentID);
+    READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_PARENT_JOINT_INDEX, quint16, setParentJointIndex);
     READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_VISIBLE, bool, setVisible);
     READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_NAME, QString, setName);
     READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_LOCKED, bool, setLocked);
@@ -3485,8 +3524,6 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int
     READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_LAST_EDITED_BY, QUuid, setLastEditedBy);
     // READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_ENTITY_HOST_TYPE, entity::HostType, setEntityHostType);            // not sent over the wire
     // READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_OWNING_AVATAR_ID, QUuid, setOwningAvatarID);                       // not sent over the wire
-    READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_PARENT_ID, QUuid, setParentID);
-    READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_PARENT_JOINT_INDEX, quint16, setParentJointIndex);
     READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_QUERY_AA_CUBE, AACube, setQueryAACube);
     READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_CAN_CAST_SHADOW, bool, setCanCastShadow);
     // READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_VISIBLE_IN_SECONDARY_CAMERA, bool, setIsVisibleInSecondaryCamera); // not sent over the wire
@@ -3593,6 +3630,7 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int
         READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_TEXTURES, QString, setTextures);
 
         READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_MODEL_URL, QString, setModelURL);
+        READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_MODEL_SCALE, vec3, setModelScale);
         READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_JOINT_ROTATIONS_SET, QVector, setJointRotationsSet);
         READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_JOINT_ROTATIONS, QVector, setJointRotations);
         READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_JOINT_TRANSLATIONS_SET, QVector, setJointTranslationsSet);
@@ -3648,6 +3686,7 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int
         READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_SKYBOX_MODE, uint32_t, setSkyboxMode);
         READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_HAZE_MODE, uint32_t, setHazeMode);
         READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_BLOOM_MODE, uint32_t, setBloomMode);
+        READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_AVATAR_PRIORITY, uint32_t, setAvatarPriority);
     }
 
     if (properties.getType() == EntityTypes::PolyVox) {
@@ -3878,6 +3917,8 @@ bool EntityItemProperties::decodeCloneEntityMessage(const QByteArray& buffer, in
 void EntityItemProperties::markAllChanged() {
     // Core
     _simulationOwnerChanged = true;
+    _parentIDChanged = true;
+    _parentJointIndexChanged = true;
     _visibleChanged = true;
     _nameChanged = true;
     _lockedChanged = true;
@@ -3892,8 +3933,6 @@ void EntityItemProperties::markAllChanged() {
     _lastEditedByChanged = true;
     _entityHostTypeChanged = true;
     _owningAvatarIDChanged = true;
-    _parentIDChanged = true;
-    _parentJointIndexChanged = true;
     _queryAACubeChanged = true;
     _canCastShadowChanged = true;
     _isVisibleInSecondaryCameraChanged = true;
@@ -3989,6 +4028,7 @@ void EntityItemProperties::markAllChanged() {
 
     // Model
     _modelURLChanged = true;
+    _modelScaleChanged = true;
     _jointRotationsSetChanged = true;
     _jointRotationsChanged = true;
     _jointTranslationsSetChanged = true;
@@ -4030,6 +4070,7 @@ void EntityItemProperties::markAllChanged() {
     _skyboxModeChanged = true;
     _hazeModeChanged = true;
     _bloomModeChanged = true;
+    _avatarPriorityChanged = true;
 
     // Polyvox
     _voxelVolumeSizeChanged = true;
@@ -4168,7 +4209,7 @@ void EntityItemProperties::copySimulationRestrictedProperties(const EntityItemPo
         setAcceleration(entity->getAcceleration());
     }
     if (!_localDimensionsChanged && !_dimensionsChanged) {
-        setDimensions(entity->getScaledDimensions());
+        setLocalDimensions(entity->getScaledDimensions());
     }
 }
 
@@ -4225,6 +4266,12 @@ QList EntityItemProperties::listChangedProperties() {
     if (simulationOwnerChanged()) {
         out += "simulationOwner";
     }
+    if (parentIDChanged()) {
+        out += "parentID";
+    }
+    if (parentJointIndexChanged()) {
+        out += "parentJointIndex";
+    }
     if (visibleChanged()) {
         out += "visible";
     }
@@ -4267,12 +4314,6 @@ QList EntityItemProperties::listChangedProperties() {
     if (owningAvatarIDChanged()) {
         out += "owningAvatarID";
     }
-    if (parentIDChanged()) {
-        out += "parentID";
-    }
-    if (parentJointIndexChanged()) {
-        out += "parentJointIndex";
-    }
     if (queryAACubeChanged()) {
         out += "queryAACube";
     }
@@ -4526,6 +4567,9 @@ QList EntityItemProperties::listChangedProperties() {
     if (modelURLChanged()) {
         out += "modelURL";
     }
+    if (modelScaleChanged()) {
+        out += "scale";
+    }
     if (jointRotationsSetChanged()) {
         out += "jointRotationsSet";
     }
@@ -4625,6 +4669,9 @@ QList EntityItemProperties::listChangedProperties() {
     if (bloomModeChanged()) {
         out += "bloomMode";
     }
+    if (avatarPriorityChanged()) {
+        out += "avatarPriority";
+    }
 
     // Polyvox
     if (voxelVolumeSizeChanged()) {
diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h
index 712f2d120f..bc1784c93b 100644
--- a/libraries/entities/src/EntityItemProperties.h
+++ b/libraries/entities/src/EntityItemProperties.h
@@ -161,6 +161,8 @@ public:
 
     // Core Properties
     DEFINE_PROPERTY_REF(PROP_SIMULATION_OWNER, SimulationOwner, simulationOwner, SimulationOwner, SimulationOwner());
+    DEFINE_PROPERTY_REF(PROP_PARENT_ID, ParentID, parentID, QUuid, UNKNOWN_ENTITY_ID);
+    DEFINE_PROPERTY_REF(PROP_PARENT_JOINT_INDEX, ParentJointIndex, parentJointIndex, quint16, -1);
     DEFINE_PROPERTY(PROP_VISIBLE, Visible, visible, bool, ENTITY_ITEM_DEFAULT_VISIBLE);
     DEFINE_PROPERTY_REF(PROP_NAME, Name, name, QString, ENTITY_ITEM_DEFAULT_NAME);
     DEFINE_PROPERTY(PROP_LOCKED, Locked, locked, bool, ENTITY_ITEM_DEFAULT_LOCKED);
@@ -175,8 +177,6 @@ public:
     DEFINE_PROPERTY_REF(PROP_LAST_EDITED_BY, LastEditedBy, lastEditedBy, QUuid, ENTITY_ITEM_DEFAULT_LAST_EDITED_BY);
     DEFINE_PROPERTY_REF_ENUM(PROP_ENTITY_HOST_TYPE, EntityHostType, entityHostType, entity::HostType, entity::HostType::DOMAIN);
     DEFINE_PROPERTY_REF_WITH_SETTER(PROP_OWNING_AVATAR_ID, OwningAvatarID, owningAvatarID, QUuid, UNKNOWN_ENTITY_ID);
-    DEFINE_PROPERTY_REF(PROP_PARENT_ID, ParentID, parentID, QUuid, UNKNOWN_ENTITY_ID);
-    DEFINE_PROPERTY_REF(PROP_PARENT_JOINT_INDEX, ParentJointIndex, parentJointIndex, quint16, -1);
     DEFINE_PROPERTY_REF(PROP_QUERY_AA_CUBE, QueryAACube, queryAACube, AACube, AACube());
     DEFINE_PROPERTY(PROP_CAN_CAST_SHADOW, CanCastShadow, canCastShadow, bool, ENTITY_ITEM_DEFAULT_CAN_CAST_SHADOW);
     DEFINE_PROPERTY(PROP_VISIBLE_IN_SECONDARY_CAMERA, IsVisibleInSecondaryCamera, isVisibleInSecondaryCamera, bool, ENTITY_ITEM_DEFAULT_VISIBLE_IN_SECONDARY_CAMERA);
@@ -279,6 +279,7 @@ public:
 
     // Model
     DEFINE_PROPERTY_REF(PROP_MODEL_URL, ModelURL, modelURL, QString, "");
+    DEFINE_PROPERTY_REF(PROP_MODEL_SCALE, ModelScale, modelScale, glm::vec3, glm::vec3(1.0f));
     DEFINE_PROPERTY_REF(PROP_JOINT_ROTATIONS_SET, JointRotationsSet, jointRotationsSet, QVector, QVector());
     DEFINE_PROPERTY_REF(PROP_JOINT_ROTATIONS, JointRotations, jointRotations, QVector, QVector());
     DEFINE_PROPERTY_REF(PROP_JOINT_TRANSLATIONS_SET, JointTranslationsSet, jointTranslationsSet, QVector, QVector());
@@ -320,6 +321,7 @@ public:
     DEFINE_PROPERTY_REF_ENUM(PROP_AMBIENT_LIGHT_MODE, AmbientLightMode, ambientLightMode, uint32_t, (uint32_t)COMPONENT_MODE_INHERIT);
     DEFINE_PROPERTY_REF_ENUM(PROP_HAZE_MODE, HazeMode, hazeMode, uint32_t, (uint32_t)COMPONENT_MODE_INHERIT);
     DEFINE_PROPERTY_REF_ENUM(PROP_BLOOM_MODE, BloomMode, bloomMode, uint32_t, (uint32_t)COMPONENT_MODE_INHERIT);
+    DEFINE_PROPERTY_REF_ENUM(PROP_AVATAR_PRIORITY, AvatarPriority, avatarPriority, uint32_t, (uint32_t)COMPONENT_MODE_INHERIT);
 
     // Polyvox
     DEFINE_PROPERTY_REF(PROP_VOXEL_VOLUME_SIZE, VoxelVolumeSize, voxelVolumeSize, glm::vec3, PolyVoxEntityItem::DEFAULT_VOXEL_VOLUME_SIZE);
@@ -680,6 +682,8 @@ inline QDebug operator<<(QDebug debug, const EntityItemProperties& properties) {
     DEBUG_PROPERTY_IF_CHANGED(debug, properties, GhostingAllowed, ghostingAllowed, "");
     DEBUG_PROPERTY_IF_CHANGED(debug, properties, FilterURL, filterURL, "");
 
+    DEBUG_PROPERTY_IF_CHANGED(debug, properties, AvatarPriority, avatarPriority, "");
+
     DEBUG_PROPERTY_IF_CHANGED(debug, properties, EntityHostTypeAsString, entityHostType, "");
     DEBUG_PROPERTY_IF_CHANGED(debug, properties, OwningAvatarID, owningAvatarID, "");
 
diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h
index 093df92dc1..6d1c3a1df8 100644
--- a/libraries/entities/src/EntityPropertyFlags.h
+++ b/libraries/entities/src/EntityPropertyFlags.h
@@ -20,6 +20,8 @@ enum EntityPropertyList {
 
     // Core properties
     PROP_SIMULATION_OWNER,
+    PROP_PARENT_ID,
+    PROP_PARENT_JOINT_INDEX,
     PROP_VISIBLE,
     PROP_NAME,
     PROP_LOCKED,
@@ -34,8 +36,6 @@ enum EntityPropertyList {
     PROP_LAST_EDITED_BY,
     PROP_ENTITY_HOST_TYPE,            // not sent over the wire
     PROP_OWNING_AVATAR_ID,            // not sent over the wire
-    PROP_PARENT_ID,
-    PROP_PARENT_JOINT_INDEX,
     PROP_QUERY_AA_CUBE,
     PROP_CAN_CAST_SHADOW,
     PROP_VISIBLE_IN_SECONDARY_CAMERA, // not sent over the wire
@@ -156,6 +156,7 @@ enum EntityPropertyList {
     PROP_DERIVED_28,
     PROP_DERIVED_29,
     PROP_DERIVED_30,
+    PROP_DERIVED_31,
 
     PROP_AFTER_LAST_ITEM,
 
@@ -202,22 +203,23 @@ enum EntityPropertyList {
 
     // Model
     PROP_MODEL_URL = PROP_DERIVED_0,
-    PROP_JOINT_ROTATIONS_SET = PROP_DERIVED_1,
-    PROP_JOINT_ROTATIONS = PROP_DERIVED_2,
-    PROP_JOINT_TRANSLATIONS_SET = PROP_DERIVED_3,
-    PROP_JOINT_TRANSLATIONS = PROP_DERIVED_4,
-    PROP_RELAY_PARENT_JOINTS = PROP_DERIVED_5,
-    PROP_GROUP_CULLED = PROP_DERIVED_6,
+    PROP_MODEL_SCALE = PROP_DERIVED_1,
+    PROP_JOINT_ROTATIONS_SET = PROP_DERIVED_2,
+    PROP_JOINT_ROTATIONS = PROP_DERIVED_3,
+    PROP_JOINT_TRANSLATIONS_SET = PROP_DERIVED_4,
+    PROP_JOINT_TRANSLATIONS = PROP_DERIVED_5,
+    PROP_RELAY_PARENT_JOINTS = PROP_DERIVED_6,
+    PROP_GROUP_CULLED = PROP_DERIVED_7,
     // Animation
-    PROP_ANIMATION_URL = PROP_DERIVED_7,
-    PROP_ANIMATION_ALLOW_TRANSLATION = PROP_DERIVED_8,
-    PROP_ANIMATION_FPS = PROP_DERIVED_9,
-    PROP_ANIMATION_FRAME_INDEX = PROP_DERIVED_10,
-    PROP_ANIMATION_PLAYING = PROP_DERIVED_11,
-    PROP_ANIMATION_LOOP = PROP_DERIVED_12,
-    PROP_ANIMATION_FIRST_FRAME = PROP_DERIVED_13,
-    PROP_ANIMATION_LAST_FRAME = PROP_DERIVED_14,
-    PROP_ANIMATION_HOLD = PROP_DERIVED_15,
+    PROP_ANIMATION_URL = PROP_DERIVED_8,
+    PROP_ANIMATION_ALLOW_TRANSLATION = PROP_DERIVED_9,
+    PROP_ANIMATION_FPS = PROP_DERIVED_10,
+    PROP_ANIMATION_FRAME_INDEX = PROP_DERIVED_11,
+    PROP_ANIMATION_PLAYING = PROP_DERIVED_12,
+    PROP_ANIMATION_LOOP = PROP_DERIVED_13,
+    PROP_ANIMATION_FIRST_FRAME = PROP_DERIVED_14,
+    PROP_ANIMATION_LAST_FRAME = PROP_DERIVED_15,
+    PROP_ANIMATION_HOLD = PROP_DERIVED_16,
 
     // Light
     PROP_IS_SPOTLIGHT = PROP_DERIVED_0,
@@ -275,6 +277,8 @@ enum EntityPropertyList {
     PROP_SKYBOX_MODE = PROP_DERIVED_28,
     PROP_HAZE_MODE = PROP_DERIVED_29,
     PROP_BLOOM_MODE = PROP_DERIVED_30,
+    // Avatar priority
+    PROP_AVATAR_PRIORITY = PROP_DERIVED_31,
 
     // Polyvox
     PROP_VOXEL_VOLUME_SIZE = PROP_DERIVED_0,
diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp
index 150aa6b0cf..22cd26eac6 100644
--- a/libraries/entities/src/EntityScriptingInterface.cpp
+++ b/libraries/entities/src/EntityScriptingInterface.cpp
@@ -941,7 +941,7 @@ QUuid EntityScriptingInterface::editEntity(const QUuid& id, const EntityItemProp
                 auto nestable = nestableWP.lock();
                 if (nestable) {
                     NestableType nestableType = nestable->getNestableType();
-                    if (nestableType == NestableType::Overlay || nestableType == NestableType::Avatar) {
+                    if (nestableType == NestableType::Avatar) {
                         qCWarning(entities) << "attempted edit on non-entity: " << id << nestable->getName();
                         return QUuid(); // null script value to indicate failure
                     }
diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp
index cfae8e250b..fffcd943c3 100644
--- a/libraries/entities/src/EntityTree.cpp
+++ b/libraries/entities/src/EntityTree.cpp
@@ -52,7 +52,15 @@ EntityTree::EntityTree(bool shouldReaverage) :
 }
 
 EntityTree::~EntityTree() {
-    eraseAllOctreeElements(false);
+    // NOTE: to eraseAllOctreeElements() in this context is useless because
+    // any OctreeElements in the tree still have shared backpointers to this Tree
+    // which means the dtor never would have been called in the first place!
+    //
+    // I'm keeping this useless commented-out line to remind us:
+    // we don't need shared pointer overhead for EntityTrees.
+    // TODO: EntityTreeElement::_tree should be raw back pointer.
+    // AND: EntityItem::_element should be a raw back pointer.
+    //eraseAllOctreeElements(false); // KEEP THIS
 }
 
 void EntityTree::setEntityScriptSourceWhitelist(const QString& entityScriptSourceWhitelist) { 
@@ -70,56 +78,62 @@ OctreeElementPointer EntityTree::createNewElement(unsigned char* octalCode) {
     return std::static_pointer_cast(newElement);
 }
 
-void EntityTree::eraseNonLocalEntities() {
+void EntityTree::eraseDomainAndNonOwnedEntities() {
     emit clearingEntities();
 
     if (_simulation) {
-        // This will clear all entities host types including local entities, because local entities
-        // are not in the physics simulation
+        // local entities are not in the simulation, so we clear ALL
         _simulation->clearEntities();
     }
-    _staleProxies.clear();
-    QHash localMap;
-    localMap.swap(_entityMap);
-    QHash savedEntities;
+
     this->withWriteLock([&] {
-        foreach(EntityItemPointer entity, localMap) {
+        QHash savedEntities;
+        // NOTE: lock the Tree first, then lock the _entityMap.
+        // It should never be done the other way around.
+        QReadLocker locker(&_entityMapLock);
+        foreach(EntityItemPointer entity, _entityMap) {
             EntityTreeElementPointer element = entity->getElement();
             if (element) {
-                element->cleanupNonLocalEntities();
+                element->cleanupDomainAndNonOwnedEntities();
             }
 
-            if (entity->isLocalEntity()) {
+            if (entity->isLocalEntity() || (entity->isAvatarEntity() && entity->getOwningAvatarID() == getMyAvatarSessionUUID())) {
                 savedEntities[entity->getEntityItemID()] = entity;
+            } else {
+                int32_t spaceIndex = entity->getSpaceIndex();
+                if (spaceIndex != -1) {
+                    // stale spaceIndices will be freed later
+                    _staleProxies.push_back(spaceIndex);
+                }
             }
         }
+        _entityMap.swap(savedEntities);
     });
-    localMap.clear();
-    _entityMap = savedEntities;
 
     resetClientEditStats();
     clearDeletedEntities();
 
     {
         QWriteLocker locker(&_needsParentFixupLock);
-        QVector localEntitiesNeedsParentFixup;
+        QVector needParentFixup;
 
         foreach (EntityItemWeakPointer entityItem, _needsParentFixup) {
-            if (!entityItem.expired() && entityItem.lock()->isLocalEntity()) {
-                localEntitiesNeedsParentFixup.push_back(entityItem);
+            auto entity = entityItem.lock();
+            if (entity && (entity->isLocalEntity() || (entity->isAvatarEntity() && entity->getOwningAvatarID() == getMyAvatarSessionUUID()))) {
+                needParentFixup.push_back(entityItem);
             }
         }
 
-        _needsParentFixup = localEntitiesNeedsParentFixup;
+        _needsParentFixup = needParentFixup;
     }
 }
+
 void EntityTree::eraseAllOctreeElements(bool createNewRoot) {
     emit clearingEntities();
 
     if (_simulation) {
         _simulation->clearEntities();
     }
-    _staleProxies.clear();
     QHash localMap;
     localMap.swap(_entityMap);
     this->withWriteLock([&] {
@@ -128,6 +142,11 @@ void EntityTree::eraseAllOctreeElements(bool createNewRoot) {
             if (element) {
                 element->cleanupEntities();
             }
+            int32_t spaceIndex = entity->getSpaceIndex();
+            if (spaceIndex != -1) {
+                // assume stale spaceIndices will be freed later
+                _staleProxies.push_back(spaceIndex);
+            }
         }
     });
     localMap.clear();
@@ -764,9 +783,9 @@ void EntityTree::processRemovedEntities(const DeleteEntityOperator& theOperator)
             _simulation->prepareEntityForDelete(theEntity);
         }
 
-        // keep a record of valid stale spaceIndices so they can be removed from the Space
         int32_t spaceIndex = theEntity->getSpaceIndex();
         if (spaceIndex != -1) {
+            // stale spaceIndices will be freed later
             _staleProxies.push_back(spaceIndex);
         }
     }
@@ -2020,6 +2039,8 @@ void EntityTree::fixupNeedsParentFixups() {
                                    Simulation::DIRTY_COLLISION_GROUP |
                                    Simulation::DIRTY_TRANSFORM);
             entityChanged(entity);
+            entity->locationChanged(true, false);
+
             entity->forEachDescendant([&](SpatiallyNestablePointer object) {
                 if (object->getNestableType() == NestableType::Entity) {
                     EntityItemPointer descendantEntity = std::static_pointer_cast(object);
@@ -2028,8 +2049,8 @@ void EntityTree::fixupNeedsParentFixups() {
                                                      Simulation::DIRTY_TRANSFORM);
                     entityChanged(descendantEntity);
                 }
+                object->locationChanged(true, false);
             });
-            entity->locationChanged(true);
 
             // Update our parent's bounding box
             bool success = false;
@@ -2953,40 +2974,9 @@ QStringList EntityTree::getJointNames(const QUuid& entityID) const {
     return entity->getJointNames();
 }
 
-std::function EntityTree::_addMaterialToEntityOperator = nullptr;
-std::function EntityTree::_removeMaterialFromEntityOperator = nullptr;
-std::function EntityTree::_addMaterialToAvatarOperator = nullptr;
-std::function EntityTree::_removeMaterialFromAvatarOperator = nullptr;
 std::function EntityTree::_getEntityObjectOperator = nullptr;
 std::function EntityTree::_textSizeOperator = nullptr;
-
-bool EntityTree::addMaterialToEntity(const QUuid& entityID, graphics::MaterialLayer material, const std::string& parentMaterialName) {
-    if (_addMaterialToEntityOperator) {
-        return _addMaterialToEntityOperator(entityID, material, parentMaterialName);
-    }
-    return false;
-}
-
-bool EntityTree::removeMaterialFromEntity(const QUuid& entityID, graphics::MaterialPointer material, const std::string& parentMaterialName) {
-    if (_removeMaterialFromEntityOperator) {
-        return _removeMaterialFromEntityOperator(entityID, material, parentMaterialName);
-    }
-    return false;
-}
-
-bool EntityTree::addMaterialToAvatar(const QUuid& avatarID, graphics::MaterialLayer material, const std::string& parentMaterialName) {
-    if (_addMaterialToAvatarOperator) {
-        return _addMaterialToAvatarOperator(avatarID, material, parentMaterialName);
-    }
-    return false;
-}
-
-bool EntityTree::removeMaterialFromAvatar(const QUuid& avatarID, graphics::MaterialPointer material, const std::string& parentMaterialName) {
-    if (_removeMaterialFromAvatarOperator) {
-        return _removeMaterialFromAvatarOperator(avatarID, material, parentMaterialName);
-    }
-    return false;
-}
+std::function EntityTree::_areEntityClicksCapturedOperator = nullptr;
 
 QObject* EntityTree::getEntityObject(const QUuid& id) {
     if (_getEntityObjectOperator) {
@@ -3002,13 +2992,31 @@ QSizeF EntityTree::textSize(const QUuid& id, const QString& text) {
     return QSizeF(0.0f, 0.0f);
 }
 
+bool EntityTree::areEntityClicksCaptured() {
+    if (_areEntityClicksCapturedOperator) {
+        return _areEntityClicksCapturedOperator();
+    }
+    return false;
+}
+
 void EntityTree::updateEntityQueryAACubeWorker(SpatiallyNestablePointer object, EntityEditPacketSender* packetSender,
                                                MovingEntitiesOperator& moveOperator, bool force, bool tellServer) {
     // if the queryBox has changed, tell the entity-server
     EntityItemPointer entity = std::dynamic_pointer_cast(object);
     if (entity) {
-        // NOTE: we rely on side-effects of the entity->updateQueryAACube() call in the following if() conditional:
-        if (entity->updateQueryAACube() || force) {
+        bool queryAACubeChanged = false;
+        if (!entity->hasChildren()) {
+            // updateQueryAACube will also update all ancestors' AACubes, so we only need to call this for leaf nodes
+            queryAACubeChanged = entity->updateQueryAACube();
+        } else {
+            AACube oldCube = entity->getQueryAACube();
+            object->forEachChild([&](SpatiallyNestablePointer descendant) {
+                updateEntityQueryAACubeWorker(descendant, packetSender, moveOperator, force, tellServer);
+            });
+            queryAACubeChanged = oldCube != entity->getQueryAACube();
+        }
+
+        if (queryAACubeChanged || force) {
             bool success;
             AACube newCube = entity->getQueryAACube(success);
             if (success) {
@@ -3032,10 +3040,6 @@ void EntityTree::updateEntityQueryAACubeWorker(SpatiallyNestablePointer object,
             entityChanged(entity);
         }
     }
-
-    object->forEachDescendant([&](SpatiallyNestablePointer descendant) {
-        updateEntityQueryAACubeWorker(descendant, packetSender, moveOperator, force, tellServer);
-    });
 }
 
 void EntityTree::updateEntityQueryAACube(SpatiallyNestablePointer object, EntityEditPacketSender* packetSender,
diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h
index 9df01267ea..39b3dc57c7 100644
--- a/libraries/entities/src/EntityTree.h
+++ b/libraries/entities/src/EntityTree.h
@@ -75,7 +75,7 @@ public:
     }
 
 
-    virtual void eraseNonLocalEntities() override;
+    virtual void eraseDomainAndNonOwnedEntities() override;
     virtual void eraseAllOctreeElements(bool createNewRoot = true) override;
 
     virtual void readBitstreamToTree(const unsigned char* bitstream,
@@ -255,6 +255,7 @@ public:
     QByteArray computeNonce(const QString& certID, const QString ownerKey);
     bool verifyNonce(const QString& certID, const QString& nonce, EntityItemID& id);
 
+    QUuid getMyAvatarSessionUUID() { return _myAvatar ? _myAvatar->getSessionUUID() : QUuid(); }
     void setMyAvatar(std::shared_ptr myAvatar) { _myAvatar = myAvatar; }
 
     void swapStaleProxies(std::vector& proxies) { proxies.swap(_staleProxies); }
@@ -262,22 +263,15 @@ public:
     void setIsServerlessMode(bool value) { _serverlessDomain = value; }
     bool isServerlessMode() const { return _serverlessDomain; }
 
-    static void setAddMaterialToEntityOperator(std::function addMaterialToEntityOperator) { _addMaterialToEntityOperator = addMaterialToEntityOperator; }
-    static void setRemoveMaterialFromEntityOperator(std::function removeMaterialFromEntityOperator) { _removeMaterialFromEntityOperator = removeMaterialFromEntityOperator; }
-    static bool addMaterialToEntity(const QUuid& entityID, graphics::MaterialLayer material, const std::string& parentMaterialName);
-    static bool removeMaterialFromEntity(const QUuid& entityID, graphics::MaterialPointer material, const std::string& parentMaterialName);
-
-    static void setAddMaterialToAvatarOperator(std::function addMaterialToAvatarOperator) { _addMaterialToAvatarOperator = addMaterialToAvatarOperator; }
-    static void setRemoveMaterialFromAvatarOperator(std::function removeMaterialFromAvatarOperator) { _removeMaterialFromAvatarOperator = removeMaterialFromAvatarOperator; }
-    static bool addMaterialToAvatar(const QUuid& avatarID, graphics::MaterialLayer material, const std::string& parentMaterialName);
-    static bool removeMaterialFromAvatar(const QUuid& avatarID, graphics::MaterialPointer material, const std::string& parentMaterialName);
-
     static void setGetEntityObjectOperator(std::function getEntityObjectOperator) { _getEntityObjectOperator = getEntityObjectOperator; }
     static QObject* getEntityObject(const QUuid& id);
 
     static void setTextSizeOperator(std::function textSizeOperator) { _textSizeOperator = textSizeOperator; }
     static QSizeF textSize(const QUuid& id, const QString& text);
 
+    static void setEntityClicksCapturedOperator(std::function areEntityClicksCapturedOperator) { _areEntityClicksCapturedOperator = areEntityClicksCapturedOperator; }
+    static bool areEntityClicksCaptured();
+
     std::map getNamedPaths() const { return _namedPaths; }
 
     void updateEntityQueryAACube(SpatiallyNestablePointer object, EntityEditPacketSender* packetSender,
@@ -386,12 +380,9 @@ private:
 
     std::shared_ptr _myAvatar{ nullptr };
 
-    static std::function _addMaterialToEntityOperator;
-    static std::function _removeMaterialFromEntityOperator;
-    static std::function _addMaterialToAvatarOperator;
-    static std::function _removeMaterialFromAvatarOperator;
     static std::function _getEntityObjectOperator;
     static std::function _textSizeOperator;
+    static std::function _areEntityClicksCapturedOperator;
 
     std::vector _staleProxies;
 
diff --git a/libraries/entities/src/EntityTreeElement.cpp b/libraries/entities/src/EntityTreeElement.cpp
index ce6f20262f..aab98adb52 100644
--- a/libraries/entities/src/EntityTreeElement.cpp
+++ b/libraries/entities/src/EntityTreeElement.cpp
@@ -697,11 +697,11 @@ EntityItemPointer EntityTreeElement::getEntityWithEntityItemID(const EntityItemI
     return foundEntity;
 }
 
-void EntityTreeElement::cleanupNonLocalEntities() {
+void EntityTreeElement::cleanupDomainAndNonOwnedEntities() {
     withWriteLock([&] {
         EntityItems savedEntities;
         foreach(EntityItemPointer entity, _entityItems) {
-            if (!entity->isLocalEntity()) {
+            if (!(entity->isLocalEntity() || (entity->isAvatarEntity() && entity->getOwningAvatarID() == getTree()->getMyAvatarSessionUUID()))) {
                 entity->preDelete();
                 entity->_element = NULL;
             } else {
diff --git a/libraries/entities/src/EntityTreeElement.h b/libraries/entities/src/EntityTreeElement.h
index f82eaa7fb1..f94da44138 100644
--- a/libraries/entities/src/EntityTreeElement.h
+++ b/libraries/entities/src/EntityTreeElement.h
@@ -190,7 +190,7 @@ public:
     EntityItemPointer getEntityWithEntityItemID(const EntityItemID& id) const;
     void getEntitiesInside(const AACube& box, QVector& foundEntities);
 
-    void cleanupNonLocalEntities();
+    void cleanupDomainAndNonOwnedEntities();
     void cleanupEntities(); /// called by EntityTree on cleanup this will free all entities
     bool removeEntityItem(EntityItemPointer entity, bool deletion = false);
 
diff --git a/libraries/entities/src/ImageEntityItem.cpp b/libraries/entities/src/ImageEntityItem.cpp
index 837e824f4a..090ae91277 100644
--- a/libraries/entities/src/ImageEntityItem.cpp
+++ b/libraries/entities/src/ImageEntityItem.cpp
@@ -159,7 +159,7 @@ bool ImageEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const
     glm::vec2 xyDimensions(dimensions.x, dimensions.y);
     glm::quat rotation = getWorldOrientation();
     glm::vec3 position = getWorldPosition() + rotation * (dimensions * (ENTITY_ITEM_DEFAULT_REGISTRATION_POINT - getRegistrationPoint()));
-    rotation = EntityItem::getBillboardRotation(position, rotation, _billboardMode);
+    rotation = EntityItem::getBillboardRotation(position, rotation, _billboardMode, EntityItem::getPrimaryViewFrustumPosition());
 
     if (findRayRectangleIntersection(origin, direction, rotation, position, xyDimensions, distance)) {
         glm::vec3 forward = rotation * Vectors::FRONT;
diff --git a/libraries/entities/src/LightEntityItem.cpp b/libraries/entities/src/LightEntityItem.cpp
index 88aae9691c..fcfda66319 100644
--- a/libraries/entities/src/LightEntityItem.cpp
+++ b/libraries/entities/src/LightEntityItem.cpp
@@ -55,8 +55,8 @@ void LightEntityItem::setUnscaledDimensions(const glm::vec3& value) {
     }
 }
 
-void LightEntityItem::locationChanged(bool tellPhysics) {
-    EntityItem::locationChanged(tellPhysics);
+void LightEntityItem::locationChanged(bool tellPhysics, bool tellChildren) {
+    EntityItem::locationChanged(tellPhysics, tellChildren);
     withWriteLock([&] {
         _lightPropertiesChanged = true;
     });
diff --git a/libraries/entities/src/LightEntityItem.h b/libraries/entities/src/LightEntityItem.h
index 26b74f02cd..cc64121cb3 100644
--- a/libraries/entities/src/LightEntityItem.h
+++ b/libraries/entities/src/LightEntityItem.h
@@ -74,7 +74,7 @@ public:
     static bool getLightsArePickable() { return _lightsArePickable; }
     static void setLightsArePickable(bool value) { _lightsArePickable = value; }
     
-    virtual void locationChanged(bool tellPhysics) override;
+    virtual void locationChanged(bool tellPhysics, bool tellChildren) override;
     virtual void dimensionsChanged() override;
 
     bool lightPropertiesChanged() const { return _lightPropertiesChanged; }
diff --git a/libraries/entities/src/MaterialEntityItem.cpp b/libraries/entities/src/MaterialEntityItem.cpp
index 1baa0b213a..3a363f2e83 100644
--- a/libraries/entities/src/MaterialEntityItem.cpp
+++ b/libraries/entities/src/MaterialEntityItem.cpp
@@ -16,9 +16,6 @@
 EntityItemPointer MaterialEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) {
     Pointer entity(new MaterialEntityItem(entityID), [](EntityItem* ptr) { ptr->deleteLater(); });
     entity->setProperties(properties);
-    // When you reload content, setProperties doesn't have any of the propertiesChanged flags set, so it won't trigger a material add
-    entity->removeMaterial();
-    entity->applyMaterial();
     return entity;
 }
 
@@ -27,10 +24,6 @@ MaterialEntityItem::MaterialEntityItem(const EntityItemID& entityItemID) : Entit
     _type = EntityTypes::Material;
 }
 
-MaterialEntityItem::~MaterialEntityItem() {
-    removeMaterial();
-}
-
 EntityItemProperties MaterialEntityItem::getProperties(const EntityPropertyFlags& desiredProperties, bool allowEmptyDesiredProperties) const {
     EntityItemProperties properties = EntityItem::getProperties(desiredProperties, allowEmptyDesiredProperties); // get the properties from our base class
     COPY_ENTITY_PROPERTY_TO_PROPERTIES(materialURL, getMaterialURL);
@@ -131,7 +124,6 @@ void MaterialEntityItem::debugDump() const {
     qCDebug(entities) << " MATERIAL EntityItem id:" << getEntityItemID() << "---------------------------------------------";
     qCDebug(entities) << "                   name:" << _name;
     qCDebug(entities) << "           material url:" << _materialURL;
-    qCDebug(entities) << "  current material name:" << _currentMaterialName.c_str();
     qCDebug(entities) << "  material mapping mode:" << _materialMappingMode;
     qCDebug(entities) << "        material repeat:" << _materialRepeat;
     qCDebug(entities) << "               priority:" << _priority;
@@ -154,208 +146,101 @@ void MaterialEntityItem::setUnscaledDimensions(const glm::vec3& value) {
     }
 }
 
-std::shared_ptr MaterialEntityItem::getMaterial() const {
-    auto material = _parsedMaterials.networkMaterials.find(_currentMaterialName);
-    if (material != _parsedMaterials.networkMaterials.end()) {
-        return material->second;
-    } else {
-        return nullptr;
-    }
+QString MaterialEntityItem::getMaterialURL() const {
+    return resultWithReadLock([&] {
+        return _materialURL;
+    });
 }
 
-void MaterialEntityItem::setMaterialURL(const QString& materialURLString, bool materialDataChanged) {
-    bool usingMaterialData = materialDataChanged || materialURLString.startsWith("materialData");
-    if (_materialURL != materialURLString || (usingMaterialData && materialDataChanged)) {
-        removeMaterial();
-        _materialURL = materialURLString;
-
-        if (materialURLString.contains("?")) {
-            auto split = materialURLString.split("?");
-            _currentMaterialName = split.last().toStdString();
-        }
-
-        if (usingMaterialData) {
-            _parsedMaterials = NetworkMaterialResource::parseJSONMaterials(QJsonDocument::fromJson(getMaterialData().toUtf8()), materialURLString);
-
-            // Since our material changed, the current name might not be valid anymore, so we need to update
-            setCurrentMaterialName(_currentMaterialName);
-            applyMaterial();
-        } else {
-            _networkMaterial = MaterialCache::instance().getMaterial(materialURLString);
-            auto onMaterialRequestFinished = [&](bool success) {
-                if (success) {
-                    _parsedMaterials = _networkMaterial->parsedMaterials;
-
-                    setCurrentMaterialName(_currentMaterialName);
-                    applyMaterial();
-                }
-            };
-            if (_networkMaterial) {
-                if (_networkMaterial->isLoaded()) {
-                    onMaterialRequestFinished(!_networkMaterial->isFailed());
-                } else {
-                    connect(_networkMaterial.data(), &Resource::finished, this, onMaterialRequestFinished);
-                }
-            }
-        }
-    }
+void MaterialEntityItem::setMaterialURL(const QString& materialURL) {
+    withWriteLock([&] {
+        _materialURL = materialURL;
+    });
 }
 
-void MaterialEntityItem::setCurrentMaterialName(const std::string& currentMaterialName) {
-    if (_parsedMaterials.networkMaterials.find(currentMaterialName) != _parsedMaterials.networkMaterials.end()) {
-        _currentMaterialName = currentMaterialName;
-    } else if (_parsedMaterials.names.size() > 0) {
-        _currentMaterialName = _parsedMaterials.names[0];
-    }
+QString MaterialEntityItem::getMaterialData() const {
+    return resultWithReadLock([&] {
+        return _materialData;
+    });
 }
 
 void MaterialEntityItem::setMaterialData(const QString& materialData) {
-    if (_materialData != materialData) {
+    withWriteLock([&] {
         _materialData = materialData;
-        if (_materialURL.startsWith("materialData")) {
-            // Trigger material update when material data changes
-            setMaterialURL(_materialURL, true);
-        }
-    }
+    });
+}
+
+MaterialMappingMode MaterialEntityItem::getMaterialMappingMode() const {
+    return resultWithReadLock([&] {
+        return _materialMappingMode;
+    });
 }
 
 void MaterialEntityItem::setMaterialMappingMode(MaterialMappingMode mode) {
-    if (_materialMappingMode != mode) {
-        removeMaterial();
+    withWriteLock([&] {
         _materialMappingMode = mode;
-        setUnscaledDimensions(_desiredDimensions);
-        applyMaterial();
-    }
+    });
+    setUnscaledDimensions(_desiredDimensions);
 }
 
-void MaterialEntityItem::setMaterialRepeat(bool repeat) {
-    if (_materialRepeat != repeat) {
-        removeMaterial();
-        _materialRepeat = repeat;
-        applyMaterial();
-    }
-}
-
-void MaterialEntityItem::setMaterialMappingPos(const glm::vec2& materialMappingPos) {
-    if (_materialMappingPos != materialMappingPos) {
-        removeMaterial();
-        _materialMappingPos = materialMappingPos;
-        applyMaterial();
-    }
-}
-
-void MaterialEntityItem::setMaterialMappingScale(const glm::vec2& materialMappingScale) {
-    if (_materialMappingScale != materialMappingScale) {
-        removeMaterial();
-        _materialMappingScale = materialMappingScale;
-        applyMaterial();
-    }
-}
-
-void MaterialEntityItem::setMaterialMappingRot(const float& materialMappingRot) {
-    if (_materialMappingRot != materialMappingRot) {
-        removeMaterial();
-        _materialMappingRot = materialMappingRot;
-        applyMaterial();
-    }
+quint16 MaterialEntityItem::getPriority() const {
+    return resultWithReadLock([&] {
+        return _priority;
+    });
 }
 
 void MaterialEntityItem::setPriority(quint16 priority) {
-    if (_priority != priority) {
-        removeMaterial();
+    withWriteLock([&] {
         _priority = priority;
-        applyMaterial();
-    }
+    });
+}
+
+QString MaterialEntityItem::getParentMaterialName() const {
+    return resultWithReadLock([&] {
+        return _parentMaterialName;
+    });
 }
 
 void MaterialEntityItem::setParentMaterialName(const QString& parentMaterialName) {
-    if (_parentMaterialName != parentMaterialName) {
-        removeMaterial();
+    withWriteLock([&] {
         _parentMaterialName = parentMaterialName;
-        applyMaterial();
-    }
+    });
 }
 
-void MaterialEntityItem::setParentID(const QUuid& parentID) {
-    if (getParentID() != parentID) {
-        removeMaterial();
-        EntityItem::setParentID(parentID);
-        applyMaterial();
-    }
+glm::vec2 MaterialEntityItem::getMaterialMappingPos() const {
+    return resultWithReadLock([&] {
+        return _materialMappingPos;
+    });
 }
 
-void MaterialEntityItem::locationChanged(bool tellPhysics) {
-    EntityItem::locationChanged();
-    if (_materialMappingMode == MaterialMappingMode::PROJECTED) {
-        removeMaterial();
-        applyMaterial();
-    }
+void MaterialEntityItem::setMaterialMappingPos(const glm::vec2& materialMappingPos) {
+    withWriteLock([&] {
+        _materialMappingPos = materialMappingPos;
+    });
 }
 
-void MaterialEntityItem::dimensionsChanged() {
-    EntityItem::dimensionsChanged();
-    if (_materialMappingMode == MaterialMappingMode::PROJECTED) {
-        removeMaterial();
-        applyMaterial();
-    }
+glm::vec2 MaterialEntityItem::getMaterialMappingScale() const {
+    return resultWithReadLock([&] {
+        return _materialMappingScale;
+    });
 }
 
-void MaterialEntityItem::removeMaterial() {
-    graphics::MaterialPointer material = getMaterial();
-    if (!material) {
-        return;
-    }
-    QUuid parentID = getParentID();
-    if (parentID.isNull()) {
-        return;
-    }
-
-    // Our parent could be an entity or an avatar
-    if (EntityTree::removeMaterialFromEntity(parentID, material, getParentMaterialName().toStdString())) {
-        return;
-    }
-
-    if (EntityTree::removeMaterialFromAvatar(parentID, material, getParentMaterialName().toStdString())) {
-        return;
-    }
-
-    // if a remove fails, our parent is gone, so we don't need to retry
+void MaterialEntityItem::setMaterialMappingScale(const glm::vec2& materialMappingScale) {
+    withWriteLock([&] {
+        _materialMappingScale = materialMappingScale;
+    });
 }
 
-void MaterialEntityItem::applyMaterial() {
-    _retryApply = false;
-    graphics::MaterialPointer material = getMaterial();
-    QUuid parentID = getParentID();
-    if (!material || parentID.isNull()) {
-        return;
-    }
+float MaterialEntityItem::getMaterialMappingRot() const {
+    return resultWithReadLock([&] {
+        return _materialMappingRot;
+    });
+}
 
-    Transform textureTransform;
-    if (_materialMappingMode == MaterialMappingMode::UV) {
-        textureTransform.setTranslation(glm::vec3(_materialMappingPos, 0.0f));
-        textureTransform.setRotation(glm::vec3(0.0f, 0.0f, glm::radians(_materialMappingRot)));
-        textureTransform.setScale(glm::vec3(_materialMappingScale, 1.0f));
-    } else if (_materialMappingMode == MaterialMappingMode::PROJECTED) {
-        textureTransform = getTransform();
-        textureTransform.postScale(getUnscaledDimensions());
-        // Pass the inverse transform here so we don't need to compute it in the shaders
-        textureTransform.evalFromRawMatrix(textureTransform.getInverseMatrix());
-    }
-    material->setTextureTransforms(textureTransform, _materialMappingMode, _materialRepeat);
-
-    graphics::MaterialLayer materialLayer = graphics::MaterialLayer(material, getPriority());
-
-    // Our parent could be an entity or an avatar
-    if (EntityTree::addMaterialToEntity(parentID, materialLayer, getParentMaterialName().toStdString())) {
-        return;
-    }
-
-    if (EntityTree::addMaterialToAvatar(parentID, materialLayer, getParentMaterialName().toStdString())) {
-        return;
-    }
-
-    // if we've reached this point, we couldn't find our parent, so we need to try again later
-    _retryApply = true;
+void MaterialEntityItem::setMaterialMappingRot(float materialMappingRot) {
+    withWriteLock([&] {
+        _materialMappingRot = materialMappingRot;
+    });
 }
 
 AACube MaterialEntityItem::calculateInitialQueryAACube(bool& success) {
@@ -372,18 +257,3 @@ AACube MaterialEntityItem::calculateInitialQueryAACube(bool& success) {
     }
     return aaCube;
 }
-
-void MaterialEntityItem::postParentFixup() {
-    removeMaterial();
-    _queryAACubeSet = false; // force an update so we contain our parent
-    updateQueryAACube();
-    applyMaterial();
-}
-
-void MaterialEntityItem::update(const quint64& now) {
-    if (_retryApply) {
-        applyMaterial();
-    }
-
-    EntityItem::update(now);
-}
diff --git a/libraries/entities/src/MaterialEntityItem.h b/libraries/entities/src/MaterialEntityItem.h
index 069c71c1d6..b9e83a7fe5 100644
--- a/libraries/entities/src/MaterialEntityItem.h
+++ b/libraries/entities/src/MaterialEntityItem.h
@@ -12,8 +12,6 @@
 #include "EntityItem.h"
 
 #include "MaterialMappingMode.h"
-#include 
-#include 
 
 class MaterialEntityItem : public EntityItem {
     using Pointer = std::shared_ptr;
@@ -21,13 +19,9 @@ public:
     static EntityItemPointer factory(const EntityItemID& entityID, const EntityItemProperties& properties);
 
     MaterialEntityItem(const EntityItemID& entityItemID);
-    ~MaterialEntityItem();
 
     ALLOW_INSTANTIATION // This class can be instantiated
 
-    void update(const quint64& now) override;
-    bool needsToCallUpdate() const override { return true; }
-
     // methods for getting/setting all properties of an entity
     virtual EntityItemProperties getProperties(const EntityPropertyFlags& desiredProperties, bool allowEmptyDesiredProperties) const override;
     virtual bool setProperties(const EntityItemProperties& properties) override;
@@ -52,44 +46,30 @@ public:
 
     virtual void setUnscaledDimensions(const glm::vec3& value) override;
 
-    QString getMaterialURL() const { return _materialURL; }
-    void setMaterialURL(const QString& materialURLString, bool materialDataChanged = false);
+    QString getMaterialURL() const;
+    void setMaterialURL(const QString& materialURL);
 
-    void setCurrentMaterialName(const std::string& currentMaterialName);
+    QString getMaterialData() const;
+    void setMaterialData(const QString& materialData);
 
-    MaterialMappingMode getMaterialMappingMode() const { return _materialMappingMode; }
+    MaterialMappingMode getMaterialMappingMode() const;
     void setMaterialMappingMode(MaterialMappingMode mode);
 
     bool getMaterialRepeat() const { return _materialRepeat; }
-    void setMaterialRepeat(bool repeat);
+    void setMaterialRepeat(bool repeat) { _materialRepeat = repeat; }
 
-    quint16 getPriority() const { return _priority; }
+    quint16 getPriority() const;
     void setPriority(quint16 priority);
 
-    QString getParentMaterialName() const { return _parentMaterialName; }
+    QString getParentMaterialName() const;
     void setParentMaterialName(const QString& parentMaterialName);
 
-    glm::vec2 getMaterialMappingPos() const { return _materialMappingPos; }
+    glm::vec2 getMaterialMappingPos() const;
     void setMaterialMappingPos(const glm::vec2& materialMappingPos);
-    glm::vec2 getMaterialMappingScale() const { return _materialMappingScale; }
+    glm::vec2 getMaterialMappingScale() const;
     void setMaterialMappingScale(const glm::vec2& materialMappingScale);
-    float getMaterialMappingRot() const { return _materialMappingRot; }
-    void setMaterialMappingRot(const float& materialMappingRot);
-
-    QString getMaterialData() const { return _materialData; }
-    void setMaterialData(const QString& materialData);
-
-    std::shared_ptr getMaterial() const;
-
-    void setParentID(const QUuid& parentID) override;
-
-    void locationChanged(bool tellPhysics) override;
-    void dimensionsChanged() override;
-
-    void applyMaterial();
-    void removeMaterial();
-
-    void postParentFixup() override;
+    float getMaterialMappingRot() const;
+    void setMaterialMappingRot(float materialMappingRot);
 
     AACube calculateInitialQueryAACube(bool& success) override;
 
@@ -128,12 +108,6 @@ private:
     float _materialMappingRot { 0 };
     QString _materialData;
 
-    NetworkMaterialResourcePointer _networkMaterial;
-    NetworkMaterialResource::ParsedMaterials _parsedMaterials;
-    std::string _currentMaterialName;
-
-    bool _retryApply { false };
-
 };
 
 #endif // hifi_MaterialEntityItem_h
diff --git a/libraries/entities/src/ModelEntityItem.cpp b/libraries/entities/src/ModelEntityItem.cpp
index e365d0a7b6..505ee26c0f 100644
--- a/libraries/entities/src/ModelEntityItem.cpp
+++ b/libraries/entities/src/ModelEntityItem.cpp
@@ -63,6 +63,7 @@ EntityItemProperties ModelEntityItem::getProperties(const EntityPropertyFlags& d
     COPY_ENTITY_PROPERTY_TO_PROPERTIES(textures, getTextures);
 
     COPY_ENTITY_PROPERTY_TO_PROPERTIES(modelURL, getModelURL);
+    COPY_ENTITY_PROPERTY_TO_PROPERTIES(modelScale, getModelScale);
     COPY_ENTITY_PROPERTY_TO_PROPERTIES(jointRotationsSet, getJointRotationsSet);
     COPY_ENTITY_PROPERTY_TO_PROPERTIES(jointRotations, getJointRotations);
     COPY_ENTITY_PROPERTY_TO_PROPERTIES(jointTranslationsSet, getJointTranslationsSet);
@@ -85,6 +86,7 @@ bool ModelEntityItem::setProperties(const EntityItemProperties& properties) {
     SET_ENTITY_PROPERTY_FROM_PROPERTIES(textures, setTextures);
 
     SET_ENTITY_PROPERTY_FROM_PROPERTIES(modelURL, setModelURL);
+    SET_ENTITY_PROPERTY_FROM_PROPERTIES(modelScale, setModelScale);
     SET_ENTITY_PROPERTY_FROM_PROPERTIES(jointRotationsSet, setJointRotationsSet);
     SET_ENTITY_PROPERTY_FROM_PROPERTIES(jointRotations, setJointRotations);
     SET_ENTITY_PROPERTY_FROM_PROPERTIES(jointTranslationsSet, setJointTranslationsSet);
@@ -128,6 +130,7 @@ int ModelEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data,
     READ_ENTITY_PROPERTY(PROP_TEXTURES, QString, setTextures);
 
     READ_ENTITY_PROPERTY(PROP_MODEL_URL, QString, setModelURL);
+    READ_ENTITY_PROPERTY(PROP_MODEL_SCALE, glm::vec3, setModelScale);
     READ_ENTITY_PROPERTY(PROP_JOINT_ROTATIONS_SET, QVector, setJointRotationsSet);
     READ_ENTITY_PROPERTY(PROP_JOINT_ROTATIONS, QVector, setJointRotations);
     READ_ENTITY_PROPERTY(PROP_JOINT_TRANSLATIONS_SET, QVector, setJointTranslationsSet);
@@ -165,6 +168,7 @@ EntityPropertyFlags ModelEntityItem::getEntityProperties(EncodeBitstreamParams&
     requestedProperties += PROP_TEXTURES;
 
     requestedProperties += PROP_MODEL_URL;
+    requestedProperties += PROP_MODEL_SCALE;
     requestedProperties += PROP_JOINT_ROTATIONS_SET;
     requestedProperties += PROP_JOINT_ROTATIONS;
     requestedProperties += PROP_JOINT_TRANSLATIONS_SET;
@@ -192,6 +196,7 @@ void ModelEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBit
     APPEND_ENTITY_PROPERTY(PROP_TEXTURES, getTextures());
 
     APPEND_ENTITY_PROPERTY(PROP_MODEL_URL, getModelURL());
+    APPEND_ENTITY_PROPERTY(PROP_MODEL_SCALE, getModelScale());
     APPEND_ENTITY_PROPERTY(PROP_JOINT_ROTATIONS_SET, getJointRotationsSet());
     APPEND_ENTITY_PROPERTY(PROP_JOINT_ROTATIONS, getJointRotations());
     APPEND_ENTITY_PROPERTY(PROP_JOINT_TRANSLATIONS_SET, getJointTranslationsSet());
@@ -296,6 +301,31 @@ void ModelEntityItem::setModelURL(const QString& url) {
     });
 }
 
+glm::vec3 ModelEntityItem::getScaledDimensions() const {
+    glm::vec3 parentScale =  getTransform().getScale();
+    return _unscaledDimensions * parentScale;
+}
+
+void ModelEntityItem::setScaledDimensions(const glm::vec3& value) {
+    glm::vec3 parentScale = getTransform().getScale();
+    setUnscaledDimensions(value / parentScale);
+}
+
+const Transform ModelEntityItem::getTransform() const {
+    bool success;
+    return getTransform(success);
+}
+
+const Transform ModelEntityItem::getTransform(bool& success, int depth) const {
+    const Transform parentTransform = getParentTransform(success, depth);
+    Transform localTransform = getLocalTransform();
+    localTransform.postScale(getModelScale());
+
+    Transform worldTransform;
+    Transform::mult(worldTransform, parentTransform, localTransform);
+
+    return worldTransform;
+}
 void ModelEntityItem::setCompoundShapeURL(const QString& url) {
     withWriteLock([&] {
         if (_compoundShapeURL.get() != url) {
@@ -708,3 +738,15 @@ bool ModelEntityItem::applyNewAnimationProperties(AnimationPropertyGroup newProp
     }
     return somethingChanged;
 }
+
+glm::vec3 ModelEntityItem::getModelScale() const {
+    return resultWithReadLock([&] {
+        return _modelScale;
+    });
+}
+
+void ModelEntityItem::setModelScale(const glm::vec3& modelScale) {
+    withWriteLock([&] {
+        _modelScale = modelScale;
+    });
+}
diff --git a/libraries/entities/src/ModelEntityItem.h b/libraries/entities/src/ModelEntityItem.h
index 649a6cb50f..08468617ba 100644
--- a/libraries/entities/src/ModelEntityItem.h
+++ b/libraries/entities/src/ModelEntityItem.h
@@ -66,6 +66,12 @@ public:
     static const QString DEFAULT_MODEL_URL;
     QString getModelURL() const;
 
+    virtual glm::vec3 getScaledDimensions() const override;
+    virtual void setScaledDimensions(const glm::vec3& value) override;
+
+    virtual const Transform getTransform(bool& success, int depth = 0) const override;
+    virtual const Transform getTransform() const override;
+
     static const QString DEFAULT_COMPOUND_SHAPE_URL;
     QString getCompoundShapeURL() const;
 
@@ -126,6 +132,9 @@ public:
     QVector getJointTranslations() const;
     QVector getJointTranslationsSet() const;
 
+    glm::vec3 getModelScale() const;
+    void setModelScale(const glm::vec3& modelScale);
+
 private:
     void setAnimationSettings(const QString& value); // only called for old bitstream format
     bool applyNewAnimationProperties(AnimationPropertyGroup newProperties);
@@ -155,6 +164,7 @@ protected:
     int _lastKnownCurrentFrame{-1};
 
     glm::u8vec3 _color;
+    glm::vec3 _modelScale { 1.0f };
     QString _modelURL;
     bool _relayParentJoints;
     bool _groupCulled { false };
diff --git a/libraries/entities/src/MovingEntitiesOperator.cpp b/libraries/entities/src/MovingEntitiesOperator.cpp
index 4b908745e0..9dd5a4d206 100644
--- a/libraries/entities/src/MovingEntitiesOperator.cpp
+++ b/libraries/entities/src/MovingEntitiesOperator.cpp
@@ -55,13 +55,11 @@ void MovingEntitiesOperator::addEntityToMoveList(EntityItemPointer entity, const
             qCDebug(entities) << "    oldContainingElement->bestFitBounds(newCubeClamped):" 
                             << oldContainingElement->bestFitBounds(newCubeClamped);
         } else {
-            qCDebug(entities) << "    WARNING NO OLD CONTAINING ELEMENT!!!";
+            qCDebug(entities) << "    WARNING NO OLD CONTAINING ELEMENT for entity" << entity->getEntityItemID();
         }
     }
-    
+
     if (!oldContainingElement) {
-            qCDebug(entities) << "UNEXPECTED!!!! attempting to move entity "<< entity->getEntityItemID() 
-                            << "that has no containing element. ";
         return; // bail without adding.
     }
 
diff --git a/libraries/entities/src/ShapeEntityItem.h b/libraries/entities/src/ShapeEntityItem.h
index 28edf2e1a2..363a7f39d1 100644
--- a/libraries/entities/src/ShapeEntityItem.h
+++ b/libraries/entities/src/ShapeEntityItem.h
@@ -85,7 +85,7 @@ public:
     void setUnscaledDimensions(const glm::vec3& value) override;
 
     bool shouldBePhysical() const override { return !isDead(); }
-    
+
     bool supportsDetailedIntersection() const override;
     bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction,
                                                 OctreeElementPointer& element, float& distance,
diff --git a/libraries/entities/src/TextEntityItem.cpp b/libraries/entities/src/TextEntityItem.cpp
index bc98c61ff7..5dff645c89 100644
--- a/libraries/entities/src/TextEntityItem.cpp
+++ b/libraries/entities/src/TextEntityItem.cpp
@@ -199,7 +199,7 @@ bool TextEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const
     glm::vec2 xyDimensions(dimensions.x, dimensions.y);
     glm::quat rotation = getWorldOrientation();
     glm::vec3 position = getWorldPosition() + rotation * (dimensions * (ENTITY_ITEM_DEFAULT_REGISTRATION_POINT - getRegistrationPoint()));
-    rotation = EntityItem::getBillboardRotation(position, rotation, _billboardMode);
+    rotation = EntityItem::getBillboardRotation(position, rotation, _billboardMode, EntityItem::getPrimaryViewFrustumPosition());
 
     if (findRayRectangleIntersection(origin, direction, rotation, position, xyDimensions, distance)) {
         glm::vec3 forward = rotation * Vectors::FRONT;
diff --git a/libraries/entities/src/WebEntityItem.cpp b/libraries/entities/src/WebEntityItem.cpp
index 5a948fbfd4..0748790df9 100644
--- a/libraries/entities/src/WebEntityItem.cpp
+++ b/libraries/entities/src/WebEntityItem.cpp
@@ -180,7 +180,7 @@ bool WebEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const g
     glm::vec2 xyDimensions(dimensions.x, dimensions.y);
     glm::quat rotation = getWorldOrientation();
     glm::vec3 position = getWorldPosition() + rotation * (dimensions * (ENTITY_ITEM_DEFAULT_REGISTRATION_POINT - getRegistrationPoint()));
-    rotation = EntityItem::getBillboardRotation(position, rotation, _billboardMode);
+    rotation = EntityItem::getBillboardRotation(position, rotation, _billboardMode, EntityItem::getPrimaryViewFrustumPosition());
 
     if (findRayRectangleIntersection(origin, direction, rotation, position, xyDimensions, distance)) {
         glm::vec3 forward = rotation * Vectors::FRONT;
diff --git a/libraries/entities/src/ZoneEntityItem.cpp b/libraries/entities/src/ZoneEntityItem.cpp
index 7b0491dbc0..98b18869fc 100644
--- a/libraries/entities/src/ZoneEntityItem.cpp
+++ b/libraries/entities/src/ZoneEntityItem.cpp
@@ -71,6 +71,7 @@ EntityItemProperties ZoneEntityItem::getProperties(const EntityPropertyFlags& de
     COPY_ENTITY_PROPERTY_TO_PROPERTIES(skyboxMode, getSkyboxMode);
     COPY_ENTITY_PROPERTY_TO_PROPERTIES(hazeMode, getHazeMode);
     COPY_ENTITY_PROPERTY_TO_PROPERTIES(bloomMode, getBloomMode);
+    COPY_ENTITY_PROPERTY_TO_PROPERTIES(avatarPriority, getAvatarPriority);
 
     return properties;
 }
@@ -117,6 +118,7 @@ bool ZoneEntityItem::setSubClassProperties(const EntityItemProperties& propertie
     SET_ENTITY_PROPERTY_FROM_PROPERTIES(skyboxMode, setSkyboxMode);
     SET_ENTITY_PROPERTY_FROM_PROPERTIES(hazeMode, setHazeMode);
     SET_ENTITY_PROPERTY_FROM_PROPERTIES(bloomMode, setBloomMode);
+    SET_ENTITY_PROPERTY_FROM_PROPERTIES(avatarPriority, setAvatarPriority);
 
     somethingChanged = somethingChanged || _keyLightPropertiesChanged || _ambientLightPropertiesChanged ||
         _skyboxPropertiesChanged || _hazePropertiesChanged || _bloomPropertiesChanged;
@@ -192,6 +194,7 @@ int ZoneEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data,
     READ_ENTITY_PROPERTY(PROP_SKYBOX_MODE, uint32_t, setSkyboxMode);
     READ_ENTITY_PROPERTY(PROP_HAZE_MODE, uint32_t, setHazeMode);
     READ_ENTITY_PROPERTY(PROP_BLOOM_MODE, uint32_t, setBloomMode);
+    READ_ENTITY_PROPERTY(PROP_AVATAR_PRIORITY, uint32_t, setAvatarPriority);
 
     return bytesRead;
 }
@@ -211,6 +214,7 @@ EntityPropertyFlags ZoneEntityItem::getEntityProperties(EncodeBitstreamParams& p
     requestedProperties += PROP_FLYING_ALLOWED;
     requestedProperties += PROP_GHOSTING_ALLOWED;
     requestedProperties += PROP_FILTER_URL;
+    requestedProperties += PROP_AVATAR_PRIORITY;
 
     requestedProperties += PROP_KEY_LIGHT_MODE;
     requestedProperties += PROP_AMBIENT_LIGHT_MODE;
@@ -256,6 +260,7 @@ void ZoneEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBits
     APPEND_ENTITY_PROPERTY(PROP_SKYBOX_MODE, (uint32_t)getSkyboxMode());
     APPEND_ENTITY_PROPERTY(PROP_HAZE_MODE, (uint32_t)getHazeMode());
     APPEND_ENTITY_PROPERTY(PROP_BLOOM_MODE, (uint32_t)getBloomMode());
+    APPEND_ENTITY_PROPERTY(PROP_AVATAR_PRIORITY, getAvatarPriority());
 }
 
 void ZoneEntityItem::debugDump() const {
@@ -269,6 +274,7 @@ void ZoneEntityItem::debugDump() const {
     qCDebug(entities) << "   _ambientLightMode:" << EntityItemProperties::getComponentModeAsString(_ambientLightMode);
     qCDebug(entities) << "         _skyboxMode:" << EntityItemProperties::getComponentModeAsString(_skyboxMode);
     qCDebug(entities) << "          _bloomMode:" << EntityItemProperties::getComponentModeAsString(_bloomMode);
+    qCDebug(entities) << "     _avatarPriority:" << getAvatarPriority();
 
     _keyLightProperties.debugDump();
     _ambientLightProperties.debugDump();
@@ -463,3 +469,18 @@ void ZoneEntityItem::fetchCollisionGeometryResource() {
         _shapeResource = DependencyManager::get()->getCollisionGeometryResource(hullURL);
     }
 }
+
+bool ZoneEntityItem::matchesJSONFilters(const QJsonObject& jsonFilters) const {
+    // currently the only property filter we handle in ZoneEntityItem is value of avatarPriority
+
+    static const QString AVATAR_PRIORITY_PROPERTY = "avatarPriority";
+
+    // If set ignore only priority-inherit zones:
+    if (jsonFilters.contains(AVATAR_PRIORITY_PROPERTY) && jsonFilters[AVATAR_PRIORITY_PROPERTY].toBool()
+        && _avatarPriority != COMPONENT_MODE_INHERIT) {
+        return true;
+    }
+
+    // Chain to base:
+    return EntityItem::matchesJSONFilters(jsonFilters);
+}
diff --git a/libraries/entities/src/ZoneEntityItem.h b/libraries/entities/src/ZoneEntityItem.h
index 11c85dab89..df6ce50fd6 100644
--- a/libraries/entities/src/ZoneEntityItem.h
+++ b/libraries/entities/src/ZoneEntityItem.h
@@ -66,6 +66,8 @@ public:
     QString getCompoundShapeURL() const;
     virtual void setCompoundShapeURL(const QString& url);
 
+    virtual bool matchesJSONFilters(const QJsonObject& jsonFilters) const override;
+
     KeyLightPropertyGroup getKeyLightProperties() const { return resultWithReadLock([&] { return _keyLightProperties; }); }
     AmbientLightPropertyGroup getAmbientLightProperties() const { return resultWithReadLock([&] { return _ambientLightProperties; }); }
 
@@ -96,6 +98,9 @@ public:
     QString getFilterURL() const;
     void setFilterURL(const QString url); 
 
+    uint32_t getAvatarPriority() const { return _avatarPriority; }
+    void setAvatarPriority(uint32_t value) { _avatarPriority = value; }
+
     bool keyLightPropertiesChanged() const { return _keyLightPropertiesChanged; }
     bool ambientLightPropertiesChanged() const { return _ambientLightPropertiesChanged; }
     bool skyboxPropertiesChanged() const { return _skyboxPropertiesChanged; }
@@ -147,6 +152,9 @@ protected:
     bool _ghostingAllowed { DEFAULT_GHOSTING_ALLOWED };
     QString _filterURL { DEFAULT_FILTER_URL };
 
+    // Avatar-updates priority
+    uint32_t _avatarPriority { COMPONENT_MODE_INHERIT };
+
     // Dirty flags turn true when either keylight properties is changing values.
     bool _keyLightPropertiesChanged { false };
     bool _ambientLightPropertiesChanged { false };
diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp
index 9e7f422b40..5246242a1e 100644
--- a/libraries/fbx/src/FBXSerializer.cpp
+++ b/libraries/fbx/src/FBXSerializer.cpp
@@ -167,7 +167,6 @@ glm::mat4 getGlobalTransform(const QMultiMap& _connectionParen
             }
         }
     }
-
     return globalTransform;
 }
 
@@ -436,6 +435,8 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr
     hfmModel.originalURL = url;
 
     float unitScaleFactor = 1.0f;
+    glm::quat upAxisZRotation;
+    bool applyUpAxisZRotation = false;
     glm::vec3 ambientColor;
     QString hifiGlobalNodeID;
     unsigned int meshIndex = 0;
@@ -473,11 +474,22 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr
                         if (subobject.name == propertyName) {
                             static const QVariant UNIT_SCALE_FACTOR = QByteArray("UnitScaleFactor");
                             static const QVariant AMBIENT_COLOR = QByteArray("AmbientColor");
+                            static const QVariant UP_AXIS = QByteArray("UpAxis");
                             const auto& subpropName = subobject.properties.at(0);
                             if (subpropName == UNIT_SCALE_FACTOR) {
                                 unitScaleFactor = subobject.properties.at(index).toFloat();
                             } else if (subpropName == AMBIENT_COLOR) {
                                 ambientColor = getVec3(subobject.properties, index);
+                            } else if (subpropName == UP_AXIS) {
+                                constexpr int UP_AXIS_Y = 1;
+                                constexpr int UP_AXIS_Z = 2;
+                                int upAxis = subobject.properties.at(index).toInt();
+                                if (upAxis == UP_AXIS_Y) {
+                                    // No update necessary, y up is the default
+                                } else if (upAxis == UP_AXIS_Z) {
+                                    upAxisZRotation = glm::angleAxis(glm::radians(-90.0f), glm::vec3(1.0f, 0.0f, 0.0f));
+                                    applyUpAxisZRotation = true;
+                                }
                             }
                         }
                     }
@@ -1269,9 +1281,11 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr
         joint.geometricScaling = fbxModel.geometricScaling;
         joint.isSkeletonJoint = fbxModel.isLimbNode;
         hfmModel.hasSkeletonJoints = (hfmModel.hasSkeletonJoints || joint.isSkeletonJoint);
-
+        if (applyUpAxisZRotation && joint.parentIndex == -1) {
+            joint.rotation *= upAxisZRotation;
+            joint.translation = upAxisZRotation * joint.translation;
+        }
         glm::quat combinedRotation = joint.preRotation * joint.rotation * joint.postRotation;
-
         if (joint.parentIndex == -1) {
             joint.transform = hfmModel.offset * glm::translate(joint.translation) * joint.preTransform *
                 glm::mat4_cast(combinedRotation) * joint.postTransform;
@@ -1664,6 +1678,14 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr
         }
     }
 
+    if (applyUpAxisZRotation) {
+        hfmModelPtr->meshExtents.transform(glm::mat4_cast(upAxisZRotation));
+        hfmModelPtr->bindExtents.transform(glm::mat4_cast(upAxisZRotation));
+        for (auto &mesh : hfmModelPtr->meshes) {
+            mesh.modelTransform *= glm::mat4_cast(upAxisZRotation);
+            mesh.meshExtents.transform(glm::mat4_cast(upAxisZRotation));
+        }
+    }
     return hfmModelPtr;
 }
 
diff --git a/libraries/fbx/src/GLTFSerializer.cpp b/libraries/fbx/src/GLTFSerializer.cpp
index 736e7831c1..940ba69bdd 100755
--- a/libraries/fbx/src/GLTFSerializer.cpp
+++ b/libraries/fbx/src/GLTFSerializer.cpp
@@ -739,8 +739,10 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const QUrl& url) {
     //Build dependencies
     QVector> nodeDependencies(_file.nodes.size());
     int nodecount = 0;
+    bool hasChildren = false;
     foreach(auto &node, _file.nodes) {
         //nodes_transforms.push_back(getModelTransform(node));
+        hasChildren |= !node.children.isEmpty();
         foreach(int child, node.children) nodeDependencies[child].push_back(nodecount);
         nodecount++;
     }
@@ -763,17 +765,25 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const QUrl& url) {
         nodecount++;
     }
     
-    //Build default joints
-    hfmModel.joints.resize(1);
-    hfmModel.joints[0].parentIndex = -1;
-    hfmModel.joints[0].distanceToParent = 0;
-    hfmModel.joints[0].translation = glm::vec3(0, 0, 0);
-    hfmModel.joints[0].rotationMin = glm::vec3(0, 0, 0);
-    hfmModel.joints[0].rotationMax = glm::vec3(0, 0, 0);
-    hfmModel.joints[0].name = "OBJ";
-    hfmModel.joints[0].isSkeletonJoint = true;
-
-    hfmModel.jointIndices["x"] = 1;
+    HFMJoint joint;
+    joint.isSkeletonJoint = true;
+    joint.bindTransformFoundInCluster = false;
+    joint.distanceToParent = 0;
+    joint.parentIndex = -1;
+    hfmModel.joints.resize(_file.nodes.size());
+    hfmModel.jointIndices["x"] = _file.nodes.size();
+    int jointInd = 0;
+    for (auto& node : _file.nodes) {
+        int size = node.transforms.size();
+        if (hasChildren) { size--; }
+        joint.preTransform = glm::mat4(1);
+        for (int i = 0; i < size; i++) {
+            joint.preTransform = node.transforms[i] * joint.preTransform;
+        }
+        joint.name = node.name;
+        hfmModel.joints[jointInd] = joint;
+        jointInd++;
+    }
 
     //Build materials
     QVector materialIDs;
@@ -804,7 +814,7 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const QUrl& url) {
                 hfmModel.meshes.append(HFMMesh());
                 HFMMesh& mesh = hfmModel.meshes[hfmModel.meshes.size() - 1];
                 HFMCluster cluster;
-                cluster.jointIndex = 0;
+                cluster.jointIndex = nodecount;
                 cluster.inverseBindMatrix = glm::mat4(1, 0, 0, 0,
                     0, 1, 0, 0,
                     0, 0, 1, 0,
@@ -907,7 +917,7 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const QUrl& url) {
                         int stride = (accessor.type == GLTFAccessorType::VEC4) ? 4 : 3;
                         for (int n = 0; n < tangents.size() - 3; n += stride) {
                             float tanW = stride == 4 ? tangents[n + 3] : 1; 
-                            mesh.tangents.push_back(glm::vec3(tanW * tangents[n], tangents[n + 1], tangents[n + 2]));
+                            mesh.tangents.push_back(glm::vec3(tanW * tangents[n], tangents[n + 1], tanW * tangents[n + 2]));
                         }
                     } else if (key == "TEXCOORD_0") {
                         QVector texcoords;
@@ -957,16 +967,7 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const QUrl& url) {
                     mesh.meshExtents.addPoint(vertex);
                     hfmModel.meshExtents.addPoint(vertex);
                 }
-                
-                // since mesh.modelTransform seems to not have any effect I apply the transformation the model 
-                for (int h = 0; h < mesh.vertices.size(); h++) {
-                    glm::vec4 ver = glm::vec4(mesh.vertices[h], 1);
-                    if (node.transforms.size() > 0) {
-                        ver = node.transforms[0] * ver; // for model dependency should multiply also by parents transforms?
-                        mesh.vertices[h] = glm::vec3(ver[0], ver[1], ver[2]);
-                    }
-                }
-
+               
                 mesh.meshIndex = hfmModel.meshes.size();
             }
             
diff --git a/libraries/gpu/src/gpu/Color.slh b/libraries/gpu/src/gpu/Color.slh
index 65ddc0b01e..c676e66c6c 100644
--- a/libraries/gpu/src/gpu/Color.slh
+++ b/libraries/gpu/src/gpu/Color.slh
@@ -16,20 +16,39 @@
 // YCoCg =====> Luma (Y) chrominance green (Cg) and chrominance orange (Co)
 // https://software.intel.com/en-us/node/503873
 
+// sRGB ====> Linear
 float color_scalar_sRGBToLinear(float value) {
-    const float SRGB_ELBOW = 0.04045;
-
-    return mix(pow((value + 0.055) / 1.055, 2.4), value / 12.92, float(value <= SRGB_ELBOW));
+    // Same as pow(value, 2.2)
+    return mix(pow((value + 0.055) / 1.055, 2.4), value / 12.92, float(value <= 0.04045));
 }
 
 vec3 color_sRGBToLinear(vec3 srgb) {
-    return vec3(color_scalar_sRGBToLinear(srgb.r), color_scalar_sRGBToLinear(srgb.g), color_scalar_sRGBToLinear(srgb.b));
+   // return vec3(color_scalar_sRGBToLinear(srgb.r), color_scalar_sRGBToLinear(srgb.g), color_scalar_sRGBToLinear(srgb.b));
+    // Same as pow(value, 2.2)
+    return mix(pow((srgb + vec3(0.055)) / vec3(1.055), vec3(2.4)), srgb / vec3(12.92), vec3(lessThanEqual(srgb, vec3(0.04045))));
+
 }
 
 vec4 color_sRGBAToLinear(vec4 srgba) {
     return vec4(color_sRGBToLinear(srgba.xyz), srgba.w);
 }
 
+// Linear ====> sRGB
+float color_scalar_LinearTosRGB(float value) {
+    // Same as return pow(value, 1/2.2)
+    return mix(1.055 * pow(value, 0.41666) - 0.055, value * 12.92, float(value < 0.0031308));
+}
+
+vec3 color_LinearTosRGB(vec3 lrgb) {
+    // Same as return pow(lrgb, 1/2.2)
+//    return vec3(color_scalar_LinearTosRGB(lrgb.r), color_scalar_LinearTosRGB(lrgb.g), color_scalar_LinearTosRGB(lrgb.b));
+    return mix(vec3(1.055) * pow(vec3(lrgb), vec3(0.41666)) - vec3(0.055), vec3(lrgb) * vec3(12.92), vec3(lessThan(lrgb, vec3(0.0031308))));
+}
+
+vec4 color_LinearTosRGBA(vec4 lrgba) {
+    return vec4(color_LinearTosRGB(lrgba.xyz), lrgba.w);
+}
+
 vec3 color_LinearToYCoCg(vec3 rgb) {
     // Y = R/4 + G/2 + B/4
     // Co = R/2 - B/2
diff --git a/libraries/gpu/src/gpu/DrawTextureGammaLinearToSRGB.slf b/libraries/gpu/src/gpu/DrawTextureGammaLinearToSRGB.slf
new file mode 100644
index 0000000000..3ca3a92f01
--- /dev/null
+++ b/libraries/gpu/src/gpu/DrawTextureGammaLinearToSRGB.slf
@@ -0,0 +1,26 @@
+<@include gpu/Config.slh@>
+<$VERSION_HEADER$>
+//  Generated on <$_SCRIBE_DATE$>
+//
+//  DrawTextureGammaLinearToSRGB.frag
+//
+//  Draw texture 0 fetched at texcoord.xy, and apply linear to sRGB color space conversion
+//
+//  Created by Sam Gateau on 2/24/2019
+//  Copyright 2019 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/Color.slh@>
+
+
+LAYOUT(binding=0) uniform sampler2D colorMap;
+
+layout(location=0) in vec2 varTexCoord0;
+layout(location=0) out vec4 outFragColor;
+
+void main(void) {
+    outFragColor = color_LinearTosRGBA(texture(colorMap, varTexCoord0));
+}
diff --git a/libraries/gpu/src/gpu/DrawTextureGammaLinearToSRGB.slp b/libraries/gpu/src/gpu/DrawTextureGammaLinearToSRGB.slp
new file mode 100644
index 0000000000..f922364b75
--- /dev/null
+++ b/libraries/gpu/src/gpu/DrawTextureGammaLinearToSRGB.slp
@@ -0,0 +1 @@
+VERTEX DrawUnitQuadTexcoord
diff --git a/libraries/gpu/src/gpu/DrawTextureGammaSRGBToLinear.slf b/libraries/gpu/src/gpu/DrawTextureGammaSRGBToLinear.slf
new file mode 100644
index 0000000000..870967ec3a
--- /dev/null
+++ b/libraries/gpu/src/gpu/DrawTextureGammaSRGBToLinear.slf
@@ -0,0 +1,26 @@
+<@include gpu/Config.slh@>
+<$VERSION_HEADER$>
+//  Generated on <$_SCRIBE_DATE$>
+//
+//  DrawTextureGammaSRGBToLinear.frag
+//
+//  Draw texture 0 fetched at texcoord.xy, and apply sRGB to Linear color space conversion
+//
+//  Created by Sam Gateau on 2/24/2019
+//  Copyright 2019 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/Color.slh@>
+
+
+LAYOUT(binding=0) uniform sampler2D colorMap;
+
+layout(location=0) in vec2 varTexCoord0;
+layout(location=0) out vec4 outFragColor;
+
+void main(void) {
+    outFragColor = color_sRGBAToLinear(texture(colorMap, varTexCoord0));
+}
diff --git a/libraries/gpu/src/gpu/DrawTextureGammaSRGBToLinear.slp b/libraries/gpu/src/gpu/DrawTextureGammaSRGBToLinear.slp
new file mode 100644
index 0000000000..f922364b75
--- /dev/null
+++ b/libraries/gpu/src/gpu/DrawTextureGammaSRGBToLinear.slp
@@ -0,0 +1 @@
+VERTEX DrawUnitQuadTexcoord
diff --git a/libraries/gpu/src/gpu/FrameReader.cpp b/libraries/gpu/src/gpu/FrameReader.cpp
index 6e39a38097..2fe143ee90 100644
--- a/libraries/gpu/src/gpu/FrameReader.cpp
+++ b/libraries/gpu/src/gpu/FrameReader.cpp
@@ -388,10 +388,23 @@ ShaderPointer Deserializer::readShader(const json& node) {
         return nullptr;
     }
 
+    static std::map shadersIdsByName;
+    if (shadersIdsByName.empty()) {
+        for (const auto id : shader::allShaders()) {
+            const auto& shaderSource = shader::Source::get(id);
+            shadersIdsByName[shaderSource.name] = id;
+        }
+    }
+
     // FIXME support procedural shaders
     Shader::Type type = node[keys::type];
     std::string name = node[keys::name];
-    uint32_t id = node[keys::id];
+    // Using the serialized ID is bad, because it's generated at 
+    // cmake time, and can change across platforms or when 
+    // shaders are added or removed
+    // uint32_t id = node[keys::id];
+    
+    uint32_t id = shadersIdsByName[name];
     ShaderPointer result;
     switch (type) {
         //case Shader::Type::GEOMETRY:
diff --git a/libraries/gpu/src/gpu/Transform.slh b/libraries/gpu/src/gpu/Transform.slh
index 43205ba4c2..3015de7e0e 100644
--- a/libraries/gpu/src/gpu/Transform.slh
+++ b/libraries/gpu/src/gpu/Transform.slh
@@ -167,7 +167,7 @@ TransformObject getTransformObject() {
         vec4 eyeClipEdge[2]= vec4[2](vec4(-1,0,0,1), vec4(1,0,0,1));
         vec2 eyeOffsetScale = vec2(-0.5, +0.5);
         uint eyeIndex = uint(_stereoSide);
-#ifndef GPU_GLES
+#if !defined(GPU_GLES) || (defined(HAVE_EXT_clip_cull_distance) && !defined(VULKAN))
         gl_ClipDistance[0] = dot(<$clipPos$>, eyeClipEdge[eyeIndex]);
 #endif
         float newClipPosX = <$clipPos$>.x * 0.5 + eyeOffsetScale[eyeIndex] * <$clipPos$>.w;
diff --git a/libraries/graphics/src/graphics/BufferViewHelpers.cpp b/libraries/graphics/src/graphics/BufferViewHelpers.cpp
index 4c57abdfd4..301f5d8d73 100644
--- a/libraries/graphics/src/graphics/BufferViewHelpers.cpp
+++ b/libraries/graphics/src/graphics/BufferViewHelpers.cpp
@@ -257,7 +257,7 @@ template  struct GpuVec3ToGlm  : GpuToGlmAdapter  { static T get(con
     case gpu::FLOAT: view.edit(index) = value; return true;
     case gpu::NUINT8: CHECK_SIZE(glm::uint32); view.edit(index) = glm::packUnorm4x8(glm::fvec4(value,0.0f)); return true;
     case gpu::UINT8: view.edit(index) = value; return true;
-    case gpu::NINT2_10_10_10: view.edit(index) = glm::packSnorm3x10_1x2(glm::fvec4(value,0.0f)); return true;
+    case gpu::NINT2_10_10_10: view.edit(index) = glm_packSnorm3x10_1x2(glm::fvec4(value,0.0f)); return true;
     default: break;
     } error("GpuVec3ToGlm::set", view, index, hint); return false;
     }
@@ -295,7 +295,7 @@ template  struct GpuVec4ToGlm : GpuToGlmAdapter { static T get(const
     case gpu::FLOAT: view.edit(index) = value; return true;
     case gpu::HALF: CHECK_SIZE(glm::uint64); view.edit(index) = glm::packHalf4x16(value); return true;
     case gpu::UINT8: view.edit(index) = value; return true;
-    case gpu::NINT2_10_10_10: view.edit(index) = glm::packSnorm3x10_1x2(value); return true;
+    case gpu::NINT2_10_10_10: view.edit(index) = glm_packSnorm3x10_1x2(value); return true;
     case gpu::NUINT16: CHECK_SIZE(glm::uint64); view.edit(index) = glm::packUnorm4x16(value); return true;
     case gpu::NUINT8: CHECK_SIZE(glm::uint32);  view.edit(index) = glm::packUnorm4x8(value); return true;
     default: break;
diff --git a/libraries/graphics/src/graphics/BufferViewHelpers.h b/libraries/graphics/src/graphics/BufferViewHelpers.h
index 8a48c17007..3635ef64e5 100644
--- a/libraries/graphics/src/graphics/BufferViewHelpers.h
+++ b/libraries/graphics/src/graphics/BufferViewHelpers.h
@@ -46,30 +46,6 @@ namespace buffer_helpers {
     gpu::BufferView clone(const gpu::BufferView& input);
     gpu::BufferView resized(const gpu::BufferView& input, glm::uint32 numElements);
 
-    inline void packNormalAndTangent(glm::vec3 normal, glm::vec3 tangent, glm::uint32& packedNormal, glm::uint32& packedTangent) {
-        auto absNormal = glm::abs(normal);
-        auto absTangent = glm::abs(tangent);
-        normal /= glm::max(1e-6f, glm::max(glm::max(absNormal.x, absNormal.y), absNormal.z));
-        tangent /= glm::max(1e-6f, glm::max(glm::max(absTangent.x, absTangent.y), absTangent.z));
-        normal = glm::clamp(normal, -1.0f, 1.0f);
-        tangent = glm::clamp(tangent, -1.0f, 1.0f);
-        normal *= 511.0f;
-        tangent *= 511.0f;
-
-        glm::detail::i10i10i10i2 normalStruct;
-        glm::detail::i10i10i10i2 tangentStruct;
-        normalStruct.data.x = fastLrintf(normal.x);
-        normalStruct.data.y = fastLrintf(normal.y);
-        normalStruct.data.z = fastLrintf(normal.z);
-        normalStruct.data.w = 0;
-        tangentStruct.data.x = fastLrintf(tangent.x);
-        tangentStruct.data.y = fastLrintf(tangent.y);
-        tangentStruct.data.z = fastLrintf(tangent.z);
-        tangentStruct.data.w = 0;
-        packedNormal = normalStruct.pack;
-        packedTangent = tangentStruct.pack;
-    }
-
     namespace mesh {
         glm::uint32 forEachVertex(const graphics::MeshPointer& mesh, std::function func);
         bool setVertexAttributes(const graphics::MeshPointer& mesh, glm::uint32 index, const QVariantMap& attributes);
diff --git a/libraries/graphics/src/graphics/Material.h b/libraries/graphics/src/graphics/Material.h
index fdddf3640a..d24e906f98 100755
--- a/libraries/graphics/src/graphics/Material.h
+++ b/libraries/graphics/src/graphics/Material.h
@@ -456,11 +456,11 @@ public:
     graphics::MaterialKey getMaterialKey() const { return graphics::MaterialKey(_schemaBuffer.get()._key); }
     const gpu::TextureTablePointer& getTextureTable() const { return _textureTable; }
 
-    bool needsUpdate() const { return _needsUpdate; }
     void setNeedsUpdate(bool needsUpdate) { _needsUpdate = needsUpdate; }
-
     void setTexturesLoading(bool value) { _texturesLoading = value; }
-    bool areTexturesLoading() const { return _texturesLoading; }
+    void setInitialized() { _initialized = true; }
+
+    bool shouldUpdate() const { return !_initialized || _needsUpdate || _texturesLoading; }
 
     int getTextureCount() const { calculateMaterialInfo(); return _textureCount; }
     size_t getTextureSize()  const { calculateMaterialInfo(); return _textureSize; }
@@ -471,6 +471,7 @@ private:
     gpu::TextureTablePointer _textureTable { std::make_shared() };
     bool _needsUpdate { false };
     bool _texturesLoading { false };
+    bool _initialized { false };
 
     mutable size_t _textureSize { 0 };
     mutable int _textureCount { 0 };
diff --git a/libraries/hfm/src/hfm/HFM.h b/libraries/hfm/src/hfm/HFM.h
index 9f3de3302c..4f44595eaa 100644
--- a/libraries/hfm/src/hfm/HFM.h
+++ b/libraries/hfm/src/hfm/HFM.h
@@ -273,6 +273,15 @@ public:
     {}
 };
 
+class FlowData {
+public:
+    FlowData() {};
+    QVariantMap _physicsConfig;
+    QVariantMap _collisionsConfig;
+    bool shouldInitFlow() const { return _physicsConfig.size() > 0; }
+    bool shouldInitCollisions() const { return _collisionsConfig.size() > 0; }
+};
+
 /// The runtime model format.
 class Model {
 public:
@@ -319,6 +328,7 @@ public:
     QList blendshapeChannelNames;
 
     QMap jointRotationOffsets;
+    FlowData flowData;
 };
 
 };
@@ -343,6 +353,7 @@ typedef hfm::Mesh HFMMesh;
 typedef hfm::AnimationFrame HFMAnimationFrame;
 typedef hfm::Light HFMLight;
 typedef hfm::Model HFMModel;
+typedef hfm::FlowData FlowData;
 
 Q_DECLARE_METATYPE(HFMAnimationFrame)
 Q_DECLARE_METATYPE(QVector)
diff --git a/libraries/input-plugins/src/input-plugins/TouchscreenVirtualPadDevice.cpp b/libraries/input-plugins/src/input-plugins/TouchscreenVirtualPadDevice.cpp
index 247f484c95..6bb743bad0 100644
--- a/libraries/input-plugins/src/input-plugins/TouchscreenVirtualPadDevice.cpp
+++ b/libraries/input-plugins/src/input-plugins/TouchscreenVirtualPadDevice.cpp
@@ -66,7 +66,7 @@ void TouchscreenVirtualPadDevice::resize() {
         _fixedRadius = _screenDPI * 0.5f * VirtualPad::Manager::BASE_DIAMETER_PIXELS / VirtualPad::Manager::DPI;
         _fixedRadiusForCalc = _fixedRadius - _screenDPI * VirtualPad::Manager::STICK_RADIUS_PIXELS / VirtualPad::Manager::DPI;
 
-        _jumpButtonRadius = _screenDPI * VirtualPad::Manager::JUMP_BTN_TRIMMED_RADIUS_PIXELS / VirtualPad::Manager::DPI;
+        _buttonRadius = _screenDPI * VirtualPad::Manager::BTN_TRIMMED_RADIUS_PIXELS / VirtualPad::Manager::DPI;
     }
 
     auto& virtualPadManager = VirtualPad::Manager::instance();
@@ -86,11 +86,21 @@ void TouchscreenVirtualPadDevice::setupControlsPositions(VirtualPad::Manager& vi
     virtualPadManager.getLeftVirtualPad()->setFirstTouch(_moveRefTouchPoint);
 
     // Jump button
-    float jumpBtnPixelSize = _screenDPI * VirtualPad::Manager::JUMP_BTN_FULL_PIXELS / VirtualPad::Manager::DPI;
-    float rightMargin = _screenDPI * VirtualPad::Manager::JUMP_BTN_RIGHT_MARGIN_PIXELS / VirtualPad::Manager::DPI;
-    float bottomMargin = _screenDPI * VirtualPad::Manager::JUMP_BTN_BOTTOM_MARGIN_PIXELS/ VirtualPad::Manager::DPI;
-    _jumpButtonPosition = glm::vec2( eventScreen->availableSize().width() - rightMargin - jumpBtnPixelSize, eventScreen->availableSize().height() - bottomMargin - _jumpButtonRadius - _extraBottomMargin);
-    virtualPadManager.setJumpButtonPosition(_jumpButtonPosition);
+    float btnPixelSize = _screenDPI * VirtualPad::Manager::BTN_FULL_PIXELS / VirtualPad::Manager::DPI;
+    float rightMargin = _screenDPI * VirtualPad::Manager::BTN_RIGHT_MARGIN_PIXELS / VirtualPad::Manager::DPI;
+    float bottomMargin = _screenDPI * VirtualPad::Manager::BTN_BOTTOM_MARGIN_PIXELS/ VirtualPad::Manager::DPI;
+    glm::vec2 jumpButtonPosition = glm::vec2( eventScreen->availableSize().width() - rightMargin - btnPixelSize, eventScreen->availableSize().height() - bottomMargin - _buttonRadius - _extraBottomMargin);
+    glm::vec2 rbButtonPosition = glm::vec2( eventScreen->availableSize().width() - rightMargin - btnPixelSize, eventScreen->availableSize().height() - 2 * bottomMargin - 3 * _buttonRadius - _extraBottomMargin);
+
+    // Avoid generating buttons in portrait mode
+    if ( eventScreen->availableSize().width() > eventScreen->availableSize().height() && _buttonsManager.buttonsCount() == 0) {
+        _buttonsManager.addButton(TouchscreenButton(JUMP, JUMP_BUTTON, _buttonRadius, jumpButtonPosition, _inputDevice ));
+        _buttonsManager.addButton(TouchscreenButton(RB, RB_BUTTON, _buttonRadius, rbButtonPosition, _inputDevice ));
+
+        virtualPadManager.setButtonPosition(VirtualPad::Manager::Button::JUMP, jumpButtonPosition);
+        virtualPadManager.setButtonPosition(VirtualPad::Manager::Button::HANDSHAKE, rbButtonPosition);
+    }
+
 }
 
 float clip(float n, float lower, float upper) {
@@ -237,7 +247,7 @@ void TouchscreenVirtualPadDevice::touchEndEvent(const QTouchEvent* event) {
     if (!virtualPadManager.isEnabled() && !virtualPadManager.isHidden()) {
         moveTouchEnd();
         viewTouchEnd();
-        jumpTouchEnd();
+        _buttonsManager.endTouchForAll();
         return;
     }
     // touch end here is a big reset -> resets both pads
@@ -246,7 +256,7 @@ void TouchscreenVirtualPadDevice::touchEndEvent(const QTouchEvent* event) {
     debugPoints(event, " END ----------------");
     moveTouchEnd();
     viewTouchEnd();
-    jumpTouchEnd();
+    _buttonsManager.endTouchForAll();
     _inputDevice->_axisStateMap.clear();
     _inputDevice->_buttonPressedMap.clear();
 }
@@ -282,11 +292,11 @@ void TouchscreenVirtualPadDevice::touchUpdateEvent(const QTouchEvent* event) {
     const QList& tPoints = event->touchPoints();
     bool moveTouchFound = false;
     bool viewTouchFound = false;
-    bool jumpTouchFound = false;
 
     int idxMoveStartingPointCandidate = -1;
     int idxViewStartingPointCandidate = -1;
-    int idxJumpStartingPointCandidate = -1;
+
+    _buttonsManager.resetEventValues();
 
     glm::vec2 thisPoint;
     int thisPointId;
@@ -311,10 +321,7 @@ void TouchscreenVirtualPadDevice::touchUpdateEvent(const QTouchEvent* event) {
             continue;
         }
 
-        if (!jumpTouchFound && _jumpHasValidTouch && _jumpCurrentTouchId == thisPointId) {
-            // valid if it's an ongoing touch
-            jumpTouchFound = true;
-            jumpTouchUpdate(thisPoint);
+        if (_buttonsManager.processOngoingTouch(thisPoint, thisPointId)) {
             continue;
         }
 
@@ -330,18 +337,16 @@ void TouchscreenVirtualPadDevice::touchUpdateEvent(const QTouchEvent* event) {
             continue;
         }
 
-        if (!jumpTouchFound && idxJumpStartingPointCandidate == -1 && jumpTouchBeginIsValid(thisPoint) &&
-                (!_unusedTouches.count(thisPointId) || _unusedTouches[thisPointId] == JUMP )) {
-            idxJumpStartingPointCandidate = i;
+        if (_buttonsManager.findStartingTouchPointCandidate(thisPoint, thisPointId, i, _unusedTouches)) {
             continue;
         }
 
         if (moveTouchBeginIsValid(thisPoint)) {
             unusedTouchesInEvent[thisPointId] = MOVE;
-        } else if (jumpTouchBeginIsValid(thisPoint)) {
-            unusedTouchesInEvent[thisPointId] = JUMP;
         } else if (viewTouchBeginIsValid(thisPoint))  {
             unusedTouchesInEvent[thisPointId] = VIEW;
+        } else {
+            _buttonsManager.saveUnusedTouches(unusedTouchesInEvent, thisPoint, thisPointId);
         }
 
     }
@@ -370,24 +375,13 @@ void TouchscreenVirtualPadDevice::touchUpdateEvent(const QTouchEvent* event) {
             viewTouchEnd();
         }
     }
-    if (!jumpTouchFound) {
-        if (idxJumpStartingPointCandidate != -1) {
-            _jumpCurrentTouchId = tPoints[idxJumpStartingPointCandidate].id();
-            _unusedTouches.erase(_jumpCurrentTouchId);
-            thisPoint.x = tPoints[idxJumpStartingPointCandidate].pos().x();
-            thisPoint.y = tPoints[idxJumpStartingPointCandidate].pos().y();
-            jumpTouchBegin(thisPoint);
-        } else {
-            if (_jumpHasValidTouch) {
-                jumpTouchEnd();
-            }
-        }
-    }
+
+    _buttonsManager.processBeginOrEnd(thisPoint, tPoints, _unusedTouches);
 
 }
 
 bool TouchscreenVirtualPadDevice::viewTouchBeginIsValid(glm::vec2 touchPoint) {
-    return !moveTouchBeginIsValid(touchPoint) && !jumpTouchBeginIsValid(touchPoint);
+    return !moveTouchBeginIsValid(touchPoint) && _buttonsManager.touchBeginInvalidForAllButtons(touchPoint);
 }
 
 bool TouchscreenVirtualPadDevice::moveTouchBeginIsValid(glm::vec2 touchPoint) {
@@ -400,30 +394,6 @@ bool TouchscreenVirtualPadDevice::moveTouchBeginIsValid(glm::vec2 touchPoint) {
     }
 }
 
-bool TouchscreenVirtualPadDevice::jumpTouchBeginIsValid(glm::vec2 touchPoint) {
-    // position of button and boundaries
-    return glm::distance2(touchPoint, _jumpButtonPosition) < _jumpButtonRadius * _jumpButtonRadius;
-}
-
-void TouchscreenVirtualPadDevice::jumpTouchBegin(glm::vec2 touchPoint) {
-    auto& virtualPadManager = VirtualPad::Manager::instance();
-    if (virtualPadManager.isEnabled() && !virtualPadManager.isHidden()) {
-        _jumpHasValidTouch = true;
-
-        _inputDevice->_buttonPressedMap.insert(TouchButtonChannel::JUMP_BUTTON_PRESS);
-    }
-}
-
-void TouchscreenVirtualPadDevice::jumpTouchUpdate(glm::vec2 touchPoint) {}
-
-void TouchscreenVirtualPadDevice::jumpTouchEnd() {
-    if (_jumpHasValidTouch) {
-        _jumpHasValidTouch = false;
-
-        _inputDevice->_buttonPressedMap.erase(TouchButtonChannel::JUMP_BUTTON_PRESS);
-    }    
-}
-
 void TouchscreenVirtualPadDevice::moveTouchBegin(glm::vec2 touchPoint) {
     auto& virtualPadManager = VirtualPad::Manager::instance();
     if (virtualPadManager.isEnabled() && !virtualPadManager.isHidden()) {
@@ -498,7 +468,8 @@ controller::Input::NamedVector TouchscreenVirtualPadDevice::InputDevice::getAvai
         Input::NamedPair(makeInput(TouchAxisChannel::LY), "LY"),
         Input::NamedPair(makeInput(TouchAxisChannel::RX), "RX"),
         Input::NamedPair(makeInput(TouchAxisChannel::RY), "RY"),
-        Input::NamedPair(makeInput(TouchButtonChannel::JUMP_BUTTON_PRESS), "JUMP_BUTTON_PRESS")
+        Input::NamedPair(makeInput(TouchButtonChannel::JUMP), "JUMP_BUTTON_PRESS"),
+        Input::NamedPair(makeInput(TouchButtonChannel::RB), "RB")
     };
     return availableInputs;
 }
@@ -507,3 +478,146 @@ QString TouchscreenVirtualPadDevice::InputDevice::getDefaultMappingConfig() cons
     static const QString MAPPING_JSON = PathUtils::resourcesPath() + "/controllers/touchscreenvirtualpad.json";
     return MAPPING_JSON;
 }
+
+TouchscreenVirtualPadDevice::TouchscreenButton::TouchscreenButton(
+        TouchscreenVirtualPadDevice::TouchButtonChannel channelIn,
+        TouchscreenVirtualPadDevice::TouchType touchTypeIn, float buttonRadiusIn,
+        glm::vec2 buttonPositionIn, std::shared_ptr inputDeviceIn) :
+    buttonPosition(buttonPositionIn),
+    buttonRadius(buttonRadiusIn),
+    touchType(touchTypeIn),
+    channel(channelIn),
+    _inputDevice(inputDeviceIn)
+{
+}
+
+void TouchscreenVirtualPadDevice::TouchscreenButton::touchBegin(glm::vec2 touchPoint) {
+    auto& virtualPadManager = VirtualPad::Manager::instance();
+    if (virtualPadManager.isEnabled() && !virtualPadManager.isHidden()) {
+        hasValidTouch = true;
+
+        _inputDevice->_buttonPressedMap.insert(channel);
+    }
+}
+
+void TouchscreenVirtualPadDevice::TouchscreenButton::touchUpdate(glm::vec2 touchPoint) {
+
+}
+
+void TouchscreenVirtualPadDevice::TouchscreenButton::touchEnd() {
+    if (hasValidTouch) {
+        hasValidTouch = false;
+
+        _inputDevice->_buttonPressedMap.erase(channel);
+    }
+}
+
+bool TouchscreenVirtualPadDevice::TouchscreenButton::touchBeginIsValid(glm::vec2 touchPoint) {
+    return glm::distance2(touchPoint, buttonPosition) < buttonRadius * buttonRadius;
+}
+
+void TouchscreenVirtualPadDevice::TouchscreenButton::resetEventValues() {
+    _candidatePointIdx = -1;
+    _found = false;
+}
+
+TouchscreenVirtualPadDevice::TouchscreenButtonsManager::TouchscreenButtonsManager() {}
+
+void TouchscreenVirtualPadDevice::TouchscreenButtonsManager::addButton(
+        TouchscreenVirtualPadDevice::TouchscreenButton button) {
+    buttons.push_back(button);
+}
+
+void TouchscreenVirtualPadDevice::TouchscreenButtonsManager::resetEventValues() {
+    for(int i = 0; i < buttons.size(); i++) {
+        TouchscreenButton &button = buttons[i];
+        button.resetEventValues();
+    }
+}
+
+bool
+TouchscreenVirtualPadDevice::TouchscreenButtonsManager::processOngoingTouch(glm::vec2 thisPoint,
+                                                                            int thisPointId) {
+    for(int i = 0; i < buttons.size(); i++) {
+        TouchscreenButton &button = buttons[i];
+
+        if (!button._found && button.hasValidTouch && button.currentTouchId == thisPointId) {
+            // valid if it's an ongoing touch
+            button._found = true;
+            button.touchUpdate(thisPoint);
+            return true;
+        }
+    }
+    return false;
+
+}
+
+bool TouchscreenVirtualPadDevice::TouchscreenButtonsManager::findStartingTouchPointCandidate(
+        glm::vec2 thisPoint, int thisPointId, int thisPointIdx, std::map &globalUnusedTouches) {
+
+    for(int i = 0; i < buttons.size(); i++) {
+        TouchscreenButton &button = buttons[i];
+        if (!button._found && button._candidatePointIdx == -1 && button.touchBeginIsValid(thisPoint)) {
+            if (!globalUnusedTouches.count(thisPointId) ) {
+                button._candidatePointIdx = thisPointIdx;
+                return true;
+            } else if (globalUnusedTouches[thisPointId] == button.touchType) {
+                button._candidatePointIdx = thisPointIdx;
+                return true;
+            }
+        }
+    }
+    return false;
+
+}
+
+void TouchscreenVirtualPadDevice::TouchscreenButtonsManager::saveUnusedTouches(
+        std::map &unusedTouchesInEvent, glm::vec2 thisPoint,
+        int thisPointId) {
+    for(int i = 0; i < buttons.size(); i++) {
+        TouchscreenButton &button = buttons[i];
+        if (button.touchBeginIsValid(thisPoint)) {
+            unusedTouchesInEvent[thisPointId] = button.touchType;
+            return;
+        }
+    }
+
+}
+
+void TouchscreenVirtualPadDevice::TouchscreenButtonsManager::processBeginOrEnd(
+        glm::vec2 thisPoint, const QList &tPoints, std::map globalUnusedTouches) {
+    for(int i = 0; i < buttons.size(); i++) {
+        TouchscreenButton &button = buttons[i];
+        if (!button._found) {
+            if (button._candidatePointIdx != -1) {
+                button.currentTouchId = tPoints[button._candidatePointIdx].id();
+                globalUnusedTouches.erase(button.currentTouchId);
+                thisPoint.x = tPoints[button._candidatePointIdx].pos().x();
+                thisPoint.y = tPoints[button._candidatePointIdx].pos().y();
+                button.touchBegin(thisPoint);
+            } else {
+                if (button.hasValidTouch) {
+                    button.touchEnd();
+                }
+            }
+        }
+    }
+
+}
+
+void TouchscreenVirtualPadDevice::TouchscreenButtonsManager::endTouchForAll() {
+    for(int i = 0; i < buttons.size(); i++) {
+        TouchscreenButton &button = buttons[i];
+        button.touchEnd();
+    }
+}
+
+bool TouchscreenVirtualPadDevice::TouchscreenButtonsManager::touchBeginInvalidForAllButtons(glm::vec2 touchPoint) {
+    for(int i = 0; i < buttons.size(); i++) {
+        TouchscreenButton &button = buttons[i];
+        if (button.touchBeginIsValid(touchPoint)) {
+            return false;
+        }
+    }
+    return true;
+}
diff --git a/libraries/input-plugins/src/input-plugins/TouchscreenVirtualPadDevice.h b/libraries/input-plugins/src/input-plugins/TouchscreenVirtualPadDevice.h
index ef1e7a4d89..4ef1dbd4f6 100644
--- a/libraries/input-plugins/src/input-plugins/TouchscreenVirtualPadDevice.h
+++ b/libraries/input-plugins/src/input-plugins/TouchscreenVirtualPadDevice.h
@@ -15,6 +15,7 @@
 #include 
 #include "InputPlugin.h"
 #include 
+#include 
 #include "VirtualPadManager.h"
 
 class QTouchEvent;
@@ -51,7 +52,8 @@ public:
     };
 
     enum TouchButtonChannel {
-        JUMP_BUTTON_PRESS
+        JUMP,
+        RB
     };
 
 protected:
@@ -82,7 +84,60 @@ protected:
     enum TouchType {
         MOVE = 1,
         VIEW,
-        JUMP
+        JUMP_BUTTON,
+        RB_BUTTON
+    };
+
+    class TouchscreenButton {
+    public:
+
+        TouchscreenButton() {};
+
+        TouchscreenButton(TouchButtonChannel channelIn, TouchType touchTypeIn, float buttonRadiusIn, glm::vec2 buttonPositionIn,
+                          std::shared_ptr inputDeviceIn);
+
+        void touchBegin(glm::vec2 touchPoint);
+        void touchUpdate(glm::vec2 touchPoint);
+        void touchEnd();
+        bool touchBeginIsValid(glm::vec2 touchPoint);
+
+        bool hasValidTouch { false };
+        int currentTouchId;
+
+        // per event tmp values
+        int _candidatePointIdx { -1 };
+        bool _found { false };
+        void resetEventValues();
+
+        glm::vec2 buttonPosition;
+        float buttonRadius;
+        TouchType touchType;
+        TouchButtonChannel channel;
+
+        std::shared_ptr _inputDevice;
+
+    };
+
+    class TouchscreenButtonsManager {
+    public:
+
+        TouchscreenButtonsManager();
+
+        QVector buttons;
+
+        void addButton(TouchscreenButton button);
+        int buttonsCount() {
+            return buttons.size();
+        }
+
+        void resetEventValues();
+        bool processOngoingTouch(glm::vec2 thisPoint, int thisPointId);
+        bool findStartingTouchPointCandidate(glm::vec2 thisPoint, int thisPointId, int thisPointIdx, std::map &globalUnusedTouches);
+        void saveUnusedTouches(std::map &unusedTouchesInEvent, glm::vec2 thisPoint, int thisPointId);
+        void processBeginOrEnd(glm::vec2 thisPoint, const QList& tPoints, std::map globalUnusedTouches);
+
+        void endTouchForAll();
+        bool touchBeginInvalidForAllButtons(glm::vec2 touchPoint);
     };
 
     float _lastPinchScale;
@@ -101,9 +156,6 @@ protected:
     glm::vec2 _viewCurrentTouchPoint;
     int _viewCurrentTouchId;
 
-    bool _jumpHasValidTouch;
-    int _jumpCurrentTouchId;
-
     std::map _unusedTouches;
 
     int _touchPointCount;
@@ -116,8 +168,9 @@ protected:
     float _fixedRadiusForCalc;
     int _extraBottomMargin {0};
 
-    glm::vec2 _jumpButtonPosition;
-    float _jumpButtonRadius;
+    float _buttonRadius;
+
+    TouchscreenButtonsManager _buttonsManager;
 
     void moveTouchBegin(glm::vec2 touchPoint);
     void moveTouchUpdate(glm::vec2 touchPoint);
@@ -129,11 +182,6 @@ protected:
     void viewTouchEnd();
     bool viewTouchBeginIsValid(glm::vec2 touchPoint);
 
-    void jumpTouchBegin(glm::vec2 touchPoint);
-    void jumpTouchUpdate(glm::vec2 touchPoint);
-    void jumpTouchEnd();
-    bool jumpTouchBeginIsValid(glm::vec2 touchPoint);
-
     void setupControlsPositions(VirtualPad::Manager& virtualPadManager, bool force = false);
 
     void processInputDeviceForMove(VirtualPad::Manager& virtualPadManager);
diff --git a/libraries/material-networking/src/material-networking/TextureCache.cpp b/libraries/material-networking/src/material-networking/TextureCache.cpp
index 9a9720c87d..43f467266a 100644
--- a/libraries/material-networking/src/material-networking/TextureCache.cpp
+++ b/libraries/material-networking/src/material-networking/TextureCache.cpp
@@ -354,7 +354,8 @@ NetworkTexture::NetworkTexture(const NetworkTexture& other) :
     _originalHeight(other._originalHeight),
     _width(other._width),
     _height(other._height),
-    _maxNumPixels(other._maxNumPixels)
+    _maxNumPixels(other._maxNumPixels),
+    _content(other._content)
 {
     if (_width == 0 || _height == 0 ||
         other._currentlyLoadingResourceType == ResourceType::META ||
@@ -368,17 +369,30 @@ static bool isLocalUrl(const QUrl& url) {
     return (scheme == HIFI_URL_SCHEME_FILE || scheme == URL_SCHEME_QRC || scheme == RESOURCE_SCHEME);
 }
 
-void NetworkTexture::setExtra(void* extra, bool isNewExtra) {
+void NetworkTexture::setExtra(void* extra) {
     const TextureExtra* textureExtra = static_cast(extra);
-    _type = textureExtra ? textureExtra->type : image::TextureUsage::DEFAULT_TEXTURE;
     _maxNumPixels = textureExtra ? textureExtra->maxNumPixels : ABSOLUTE_MAX_TEXTURE_NUM_PIXELS;
-    _sourceChannel = textureExtra ? textureExtra->sourceChannel : image::ColorChannel::NONE;
 
-    if (isNewExtra && !_loaded) {
+    bool needsNewTextureSource = false;
+    auto type = textureExtra ? textureExtra->type : image::TextureUsage::DEFAULT_TEXTURE;
+    auto sourceChannel = textureExtra ? textureExtra->sourceChannel : image::ColorChannel::NONE;
+    if (type != _type || sourceChannel != _sourceChannel) {
+        needsNewTextureSource = true;
+    }
+    _type = type;
+    _sourceChannel = sourceChannel;
+
+    auto content = textureExtra ? textureExtra->content : QByteArray();
+    if (_content.isEmpty() && !content.isEmpty()) {
+        _content = content;
+        needsNewTextureSource = true;
+    }
+
+    if (needsNewTextureSource) {
         _startedLoading = false;
     }
 
-    if (!_textureSource || isNewExtra) {
+    if (!_textureSource || needsNewTextureSource) {
         _textureSource = std::make_shared(_url, (int)_type);
     }
     _lowestRequestedMipLevel = 0;
@@ -405,10 +419,9 @@ void NetworkTexture::setExtra(void* extra, bool isNewExtra) {
     }
 
     // if we have content, load it after we have our self pointer
-    auto content = textureExtra ? textureExtra->content : QByteArray();
-    if (!content.isEmpty()) {
+    if (!_content.isEmpty()) {
         _startedLoading = true;
-        QMetaObject::invokeMethod(this, "downloadFinished", Qt::QueuedConnection, Q_ARG(const QByteArray&, content));
+        QMetaObject::invokeMethod(this, "downloadFinished", Qt::QueuedConnection, Q_ARG(const QByteArray&, _content));
     }
 }
 
diff --git a/libraries/material-networking/src/material-networking/TextureCache.h b/libraries/material-networking/src/material-networking/TextureCache.h
index a8b152c40e..dcab527e4a 100644
--- a/libraries/material-networking/src/material-networking/TextureCache.h
+++ b/libraries/material-networking/src/material-networking/TextureCache.h
@@ -64,7 +64,7 @@ public:
 
     Q_INVOKABLE void setOriginalDescriptor(ktx::KTXDescriptor* descriptor) { _originalKtxDescriptor.reset(descriptor); }
 
-    void setExtra(void* extra, bool isNewExtra) override;
+    void setExtra(void* extra) override;
 
 signals:
     void networkTextureCreated(const QWeakPointer& self);
@@ -136,12 +136,12 @@ private:
     // mip offsets to change.
     ktx::KTXDescriptorPointer _originalKtxDescriptor;
 
-
     int _originalWidth { 0 };
     int _originalHeight { 0 };
     int _width { 0 };
     int _height { 0 };
     int _maxNumPixels { ABSOLUTE_MAX_TEXTURE_NUM_PIXELS };
+    QByteArray _content;
 
     friend class TextureCache;
 };
diff --git a/libraries/model-baker/src/model-baker/Baker.cpp b/libraries/model-baker/src/model-baker/Baker.cpp
index dfb18eef86..f55cacf0f2 100644
--- a/libraries/model-baker/src/model-baker/Baker.cpp
+++ b/libraries/model-baker/src/model-baker/Baker.cpp
@@ -20,6 +20,7 @@
 #include "CalculateBlendshapeNormalsTask.h"
 #include "CalculateBlendshapeTangentsTask.h"
 #include "PrepareJointsTask.h"
+#include "ParseFlowDataTask.h"
 
 namespace baker {
 
@@ -101,7 +102,7 @@ namespace baker {
 
     class BuildModelTask {
     public:
-        using Input = VaryingSet5, std::vector, QMap, QHash>;
+        using Input = VaryingSet6, std::vector, QMap, QHash, FlowData>;
         using Output = hfm::Model::Pointer;
         using JobModel = Job::ModelIO;
 
@@ -111,13 +112,14 @@ namespace baker {
             hfmModelOut->joints = QVector::fromStdVector(input.get2());
             hfmModelOut->jointRotationOffsets = input.get3();
             hfmModelOut->jointIndices = input.get4();
+            hfmModelOut->flowData = input.get5();
             output = hfmModelOut;
         }
     };
 
     class BakerEngineBuilder {
     public:
-        using Input = VaryingSet2;
+        using Input = VaryingSet2;
         using Output = VaryingSet2;
         using JobModel = Task::ModelIO;
         void build(JobModel& model, const Varying& input, Varying& output) {
@@ -157,19 +159,22 @@ namespace baker {
             // Parse material mapping
             const auto materialMapping = model.addJob("ParseMaterialMapping", mapping);
 
+            // Parse flow data
+            const auto flowData = model.addJob("ParseFlowData", mapping);
+
             // Combine the outputs into a new hfm::Model
             const auto buildBlendshapesInputs = BuildBlendshapesTask::Input(blendshapesPerMeshIn, normalsPerBlendshapePerMesh, tangentsPerBlendshapePerMesh).asVarying();
             const auto blendshapesPerMeshOut = model.addJob("BuildBlendshapes", buildBlendshapesInputs);
             const auto buildMeshesInputs = BuildMeshesTask::Input(meshesIn, graphicsMeshes, normalsPerMesh, tangentsPerMesh, blendshapesPerMeshOut).asVarying();
             const auto meshesOut = model.addJob("BuildMeshes", buildMeshesInputs);
-            const auto buildModelInputs = BuildModelTask::Input(hfmModelIn, meshesOut, jointsOut, jointRotationOffsets, jointIndices).asVarying();
+            const auto buildModelInputs = BuildModelTask::Input(hfmModelIn, meshesOut, jointsOut, jointRotationOffsets, jointIndices, flowData).asVarying();
             const auto hfmModelOut = model.addJob("BuildModel", buildModelInputs);
 
             output = Output(hfmModelOut, materialMapping);
         }
     };
 
-    Baker::Baker(const hfm::Model::Pointer& hfmModel, const QVariantHash& mapping) :
+    Baker::Baker(const hfm::Model::Pointer& hfmModel, const GeometryMappingPair& mapping) :
         _engine(std::make_shared(BakerEngineBuilder::JobModel::create("Baker"), std::make_shared())) {
         _engine->feedInput(0, hfmModel);
         _engine->feedInput(1, mapping);
diff --git a/libraries/model-baker/src/model-baker/Baker.h b/libraries/model-baker/src/model-baker/Baker.h
index 542be0b559..856b5f0142 100644
--- a/libraries/model-baker/src/model-baker/Baker.h
+++ b/libraries/model-baker/src/model-baker/Baker.h
@@ -17,13 +17,14 @@
 #include 
 
 #include "Engine.h"
+#include "BakerTypes.h"
 
 #include "ParseMaterialMappingTask.h"
 
 namespace baker {
     class Baker {
     public:
-        Baker(const hfm::Model::Pointer& hfmModel, const QVariantHash& mapping);
+        Baker(const hfm::Model::Pointer& hfmModel, const GeometryMappingPair& mapping);
 
         void run();
 
diff --git a/libraries/model-baker/src/model-baker/BakerTypes.h b/libraries/model-baker/src/model-baker/BakerTypes.h
index 5d14ee5420..8b80b0bde4 100644
--- a/libraries/model-baker/src/model-baker/BakerTypes.h
+++ b/libraries/model-baker/src/model-baker/BakerTypes.h
@@ -12,6 +12,7 @@
 #ifndef hifi_BakerTypes_h
 #define hifi_BakerTypes_h
 
+#include 
 #include 
 
 namespace baker {
@@ -35,6 +36,7 @@ namespace baker {
     using TangentsPerBlendshape = std::vector>;
 
     using MeshIndicesToModelNames = QHash;
+    using GeometryMappingPair = std::pair;
 };
 
 #endif // hifi_BakerTypes_h
diff --git a/libraries/model-baker/src/model-baker/BuildGraphicsMeshTask.cpp b/libraries/model-baker/src/model-baker/BuildGraphicsMeshTask.cpp
index 370add2c2e..c41431f940 100644
--- a/libraries/model-baker/src/model-baker/BuildGraphicsMeshTask.cpp
+++ b/libraries/model-baker/src/model-baker/BuildGraphicsMeshTask.cpp
@@ -125,8 +125,8 @@ void buildGraphicsMesh(const hfm::Mesh& hfmMesh, graphics::MeshPointer& graphics
 #if HFM_PACK_NORMALS
             const auto normal = normalizeDirForPacking(*normalIt);
             const auto tangent = normalizeDirForPacking(*tangentIt);
-            const auto packedNormal = glm::packSnorm3x10_1x2(glm::vec4(normal, 0.0f));
-            const auto packedTangent = glm::packSnorm3x10_1x2(glm::vec4(tangent, 0.0f));
+            const auto packedNormal = glm_packSnorm3x10_1x2(glm::vec4(normal, 0.0f));
+            const auto packedTangent = glm_packSnorm3x10_1x2(glm::vec4(tangent, 0.0f));
 #else
             const auto packedNormal = *normalIt;
             const auto packedTangent = *tangentIt;
diff --git a/libraries/model-baker/src/model-baker/ParseFlowDataTask.cpp b/libraries/model-baker/src/model-baker/ParseFlowDataTask.cpp
new file mode 100644
index 0000000000..10991ecbe6
--- /dev/null
+++ b/libraries/model-baker/src/model-baker/ParseFlowDataTask.cpp
@@ -0,0 +1,34 @@
+//
+//  Created by Luis Cuenca on 5/3/2019
+//  Copyright 2019 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 "ParseFlowDataTask.h"
+
+void ParseFlowDataTask::run(const baker::BakeContextPointer& context, const Input& mappingPair, Output& output) {
+    FlowData flowData;
+    static const QString FLOW_PHYSICS_FIELD = "flowPhysicsData";
+    static const QString FLOW_COLLISIONS_FIELD = "flowCollisionsData";
+    auto mapping = mappingPair.second;
+    for (auto mappingIter = mapping.begin(); mappingIter != mapping.end(); mappingIter++) {
+        if (mappingIter.key() == FLOW_PHYSICS_FIELD || mappingIter.key() == FLOW_COLLISIONS_FIELD) {
+            QByteArray data = mappingIter.value().toByteArray();
+            QJsonObject dataObject = QJsonDocument::fromJson(data).object();
+            if (!dataObject.isEmpty() && dataObject.keys().size() == 1) {
+                QString key = dataObject.keys()[0];
+                if (dataObject[key].isObject()) {
+                    QVariantMap dataMap = dataObject[key].toObject().toVariantMap();
+                    if (mappingIter.key() == FLOW_PHYSICS_FIELD) {
+                        flowData._physicsConfig.insert(key, dataMap);
+                    } else {
+                        flowData._collisionsConfig.insert(key, dataMap);
+                    }
+                }
+            }
+        }
+    }
+    output = flowData;
+}
diff --git a/libraries/model-baker/src/model-baker/ParseFlowDataTask.h b/libraries/model-baker/src/model-baker/ParseFlowDataTask.h
new file mode 100644
index 0000000000..65b8f7654b
--- /dev/null
+++ b/libraries/model-baker/src/model-baker/ParseFlowDataTask.h
@@ -0,0 +1,26 @@
+//
+//  Created by Luis Cuenca on 5/3/2019
+//  Copyright 2019 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_ParseFlowDataTask_h
+#define hifi_ParseFlowDataTask_h
+
+#include 
+#include "Engine.h"
+
+#include "BakerTypes.h"
+
+class ParseFlowDataTask {
+public:
+    using Input = baker::GeometryMappingPair;
+    using Output = FlowData;
+    using JobModel = baker::Job::ModelIO;
+
+    void run(const baker::BakeContextPointer& context, const Input& input, Output& output);
+};
+
+#endif // hifi_ParseFlowDataTask_h
diff --git a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp
index 7a923a3702..0a1964d8cd 100644
--- a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp
+++ b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp
@@ -10,7 +10,9 @@
 
 #include "ModelBakerLogging.h"
 
-void ParseMaterialMappingTask::run(const baker::BakeContextPointer& context, const Input& mapping, Output& output) {
+void ParseMaterialMappingTask::run(const baker::BakeContextPointer& context, const Input& input, Output& output) {
+    const auto& url = input.first;
+    const auto& mapping = input.second;
     MaterialMapping materialMapping;
 
     auto mappingIter = mapping.find("materialMap");
@@ -59,14 +61,13 @@ void ParseMaterialMappingTask::run(const baker::BakeContextPointer& context, con
                     {
                         NetworkMaterialResourcePointer materialResource = NetworkMaterialResourcePointer(new NetworkMaterialResource(), [](NetworkMaterialResource* ptr) { ptr->deleteLater(); });
                         materialResource->moveToThread(qApp->thread());
-                        // TODO: add baseURL to allow FSTs to reference relative files next to them
-                        materialResource->parsedMaterials = NetworkMaterialResource::parseJSONMaterials(QJsonDocument(mappingValue), QUrl());
+                        materialResource->parsedMaterials = NetworkMaterialResource::parseJSONMaterials(QJsonDocument(mappingValue), url);
                         materialMapping.push_back(std::pair(mapping.toStdString(), materialResource));
                     }
 
                 } else if (mappingJSON.isString()) {
                     auto mappingValue = mappingJSON.toString();
-                    materialMapping.push_back(std::pair(mapping.toStdString(), MaterialCache::instance().getMaterial(mappingValue)));
+                    materialMapping.push_back(std::pair(mapping.toStdString(), MaterialCache::instance().getMaterial(url.resolved(mappingValue))));
                 }
             }
         }
diff --git a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h
index 69e00b0324..5f5eff327d 100644
--- a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h
+++ b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h
@@ -14,12 +14,13 @@
 #include 
 
 #include "Engine.h"
+#include "BakerTypes.h"
 
 #include 
 
 class ParseMaterialMappingTask {
 public:
-    using Input = QVariantHash;
+    using Input = baker::GeometryMappingPair;
     using Output = MaterialMapping;
     using JobModel = baker::Job::ModelIO;
 
diff --git a/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp b/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp
index 3b1a57cb43..a896766058 100644
--- a/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp
+++ b/libraries/model-baker/src/model-baker/PrepareJointsTask.cpp
@@ -58,7 +58,7 @@ void PrepareJointsTask::run(const baker::BakeContextPointer& context, const Inpu
     auto& jointIndices = output.edit2();
 
     // Get joint renames
-    auto jointNameMapping = getJointNameMapping(mapping);
+    auto jointNameMapping = getJointNameMapping(mapping.second);
     // Apply joint metadata from FST file mappings
     for (const auto& jointIn : jointsIn) {
         jointsOut.push_back(jointIn);
@@ -73,7 +73,7 @@ void PrepareJointsTask::run(const baker::BakeContextPointer& context, const Inpu
     }
 
     // Get joint rotation offsets from FST file mappings
-    auto offsets = getJointRotationOffsets(mapping);
+    auto offsets = getJointRotationOffsets(mapping.second);
     for (auto itr = offsets.begin(); itr != offsets.end(); itr++) {
         QString jointName = itr.key();
         int jointIndex = jointIndices.value(jointName) - 1;
diff --git a/libraries/model-baker/src/model-baker/PrepareJointsTask.h b/libraries/model-baker/src/model-baker/PrepareJointsTask.h
index e12d8ffd2c..b18acdfceb 100644
--- a/libraries/model-baker/src/model-baker/PrepareJointsTask.h
+++ b/libraries/model-baker/src/model-baker/PrepareJointsTask.h
@@ -17,10 +17,11 @@
 #include 
 
 #include "Engine.h"
+#include "BakerTypes.h"
 
 class PrepareJointsTask {
 public:
-    using Input = baker::VaryingSet2, QVariantHash /*mapping*/>;
+    using Input = baker::VaryingSet2, baker::GeometryMappingPair /*mapping*/>;
     using Output = baker::VaryingSet3, QMap /*jointRotationOffsets*/, QHash /*jointIndices*/>;
     using JobModel = baker::Job::ModelIO;
 
diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp
index b2645d20c8..a48f96eb1b 100644
--- a/libraries/model-networking/src/model-networking/ModelCache.cpp
+++ b/libraries/model-networking/src/model-networking/ModelCache.cpp
@@ -35,11 +35,13 @@ class GeometryReader;
 
 class GeometryExtra {
 public:
-    const QVariantHash& mapping;
+    const GeometryMappingPair& mapping;
     const QUrl& textureBaseUrl;
     bool combineParts;
 };
 
+int geometryMappingPairTypeId = qRegisterMetaType("GeometryMappingPair");
+
 // From: https://stackoverflow.com/questions/41145012/how-to-hash-qvariant
 class QVariantHasher {
 public:
@@ -78,7 +80,8 @@ namespace std {
     struct hash {
         size_t operator()(const GeometryExtra& geometryExtra) const {
             size_t result = 0;
-            hash_combine(result, geometryExtra.mapping, geometryExtra.textureBaseUrl, geometryExtra.combineParts);
+            hash_combine(result, geometryExtra.mapping.first, geometryExtra.mapping.second, geometryExtra.textureBaseUrl,
+                geometryExtra.combineParts);
             return result;
         }
     };
@@ -151,7 +154,7 @@ void GeometryMappingResource::downloadFinished(const QByteArray& data) {
         }
 
         auto modelCache = DependencyManager::get();
-        GeometryExtra extra { _mapping, _textureBaseUrl, false };
+        GeometryExtra extra { GeometryMappingPair(_url, _mapping), _textureBaseUrl, false };
 
         // Get the raw GeometryResource
         _geometryResource = modelCache->getResource(url, QUrl(), &extra, std::hash()(extra)).staticCast();
@@ -191,7 +194,7 @@ void GeometryMappingResource::onGeometryMappingLoaded(bool success) {
 
 class GeometryReader : public QRunnable {
 public:
-    GeometryReader(const ModelLoader& modelLoader, QWeakPointer& resource, const QUrl& url, const QVariantHash& mapping,
+    GeometryReader(const ModelLoader& modelLoader, QWeakPointer& resource, const QUrl& url, const GeometryMappingPair& mapping,
                    const QByteArray& data, bool combineParts, const QString& webMediaType) :
         _modelLoader(modelLoader), _resource(resource), _url(url), _mapping(mapping), _data(data), _combineParts(combineParts), _webMediaType(webMediaType) {
 
@@ -204,7 +207,7 @@ private:
     ModelLoader _modelLoader;
     QWeakPointer _resource;
     QUrl _url;
-    QVariantHash _mapping;
+    GeometryMappingPair _mapping;
     QByteArray _data;
     bool _combineParts;
     QString _webMediaType;
@@ -244,7 +247,7 @@ void GeometryReader::run() {
         }
 
         HFMModel::Pointer hfmModel;
-        QVariantHash serializerMapping = _mapping;
+        QVariantHash serializerMapping = _mapping.second;
         serializerMapping["combineParts"] = _combineParts;
 
         if (_url.path().toLower().endsWith(".gz")) {
@@ -270,15 +273,14 @@ void GeometryReader::run() {
         }
 
         // Add scripts to hfmModel
-        if (!_mapping.value(SCRIPT_FIELD).isNull()) {
-            QVariantList scripts = _mapping.values(SCRIPT_FIELD);
+        if (!serializerMapping.value(SCRIPT_FIELD).isNull()) {
+            QVariantList scripts = serializerMapping.values(SCRIPT_FIELD);
             for (auto &script : scripts) {
                 hfmModel->scripts.push_back(script.toString());
             }
         }
-
         QMetaObject::invokeMethod(resource.data(), "setGeometryDefinition",
-                Q_ARG(HFMModel::Pointer, hfmModel), Q_ARG(QVariantHash, _mapping));
+                Q_ARG(HFMModel::Pointer, hfmModel), Q_ARG(GeometryMappingPair, _mapping));
     } catch (const std::exception&) {
         auto resource = _resource.toStrongRef();
         if (resource) {
@@ -309,20 +311,20 @@ public:
 
     virtual void downloadFinished(const QByteArray& data) override;
 
-    void setExtra(void* extra, bool isNewExtra) override;
+    void setExtra(void* extra) override;
 
 protected:
-    Q_INVOKABLE void setGeometryDefinition(HFMModel::Pointer hfmModel, QVariantHash mapping);
+    Q_INVOKABLE void setGeometryDefinition(HFMModel::Pointer hfmModel, const GeometryMappingPair& mapping);
 
 private:
     ModelLoader _modelLoader;
-    QVariantHash _mapping;
+    GeometryMappingPair _mapping;
     bool _combineParts;
 };
 
-void GeometryDefinitionResource::setExtra(void* extra, bool isNewExtra) {
+void GeometryDefinitionResource::setExtra(void* extra) {
     const GeometryExtra* geometryExtra = static_cast(extra);
-    _mapping = geometryExtra ? geometryExtra->mapping : QVariantHash();
+    _mapping = geometryExtra ? geometryExtra->mapping : GeometryMappingPair(QUrl(), QVariantHash());
     _textureBaseUrl = geometryExtra ? resolveTextureBaseUrl(_url, geometryExtra->textureBaseUrl) : QUrl();
     _combineParts = geometryExtra ? geometryExtra->combineParts : true;
 }
@@ -335,7 +337,7 @@ void GeometryDefinitionResource::downloadFinished(const QByteArray& data) {
     QThreadPool::globalInstance()->start(new GeometryReader(_modelLoader, _self, _effectiveBaseURL, _mapping, data, _combineParts, _request->getWebMediaType()));
 }
 
-void GeometryDefinitionResource::setGeometryDefinition(HFMModel::Pointer hfmModel, QVariantHash mapping) {
+void GeometryDefinitionResource::setGeometryDefinition(HFMModel::Pointer hfmModel, const GeometryMappingPair& mapping) {
     // Do processing on the model
     baker::Baker modelBaker(hfmModel, mapping);
     modelBaker.run();
@@ -394,11 +396,15 @@ QSharedPointer ModelCache::createResource(const QUrl& url) {
 }
 
 QSharedPointer ModelCache::createResourceCopy(const QSharedPointer& resource) {
-    return QSharedPointer(new GeometryDefinitionResource(*resource.staticCast()), &Resource::deleter);
+    if (resource->getURL().path().toLower().endsWith(".fst")) {
+        return QSharedPointer(new GeometryMappingResource(*resource.staticCast()), &Resource::deleter);
+    } else {
+        return QSharedPointer(new GeometryDefinitionResource(*resource.staticCast()), &Resource::deleter);
+    }
 }
 
 GeometryResource::Pointer ModelCache::getGeometryResource(const QUrl& url,
-                                                          const QVariantHash& mapping, const QUrl& textureBaseUrl) {
+                                                          const GeometryMappingPair& mapping, const QUrl& textureBaseUrl) {
     bool combineParts = true;
     GeometryExtra geometryExtra = { mapping, textureBaseUrl, combineParts };
     GeometryResource::Pointer resource = getResource(url, QUrl(), &geometryExtra, std::hash()(geometryExtra)).staticCast();
@@ -411,7 +417,8 @@ GeometryResource::Pointer ModelCache::getGeometryResource(const QUrl& url,
 }
 
 GeometryResource::Pointer ModelCache::getCollisionGeometryResource(const QUrl& url,
-                                                                   const QVariantHash& mapping, const QUrl& textureBaseUrl) {
+                                                                   const GeometryMappingPair& mapping,
+                                                                   const QUrl& textureBaseUrl) {
     bool combineParts = false;
     GeometryExtra geometryExtra = { mapping, textureBaseUrl, combineParts };
     GeometryResource::Pointer resource = getResource(url, QUrl(), &geometryExtra, std::hash()(geometryExtra)).staticCast();
diff --git a/libraries/model-networking/src/model-networking/ModelCache.h b/libraries/model-networking/src/model-networking/ModelCache.h
index 4cd7048dca..ca1ceaff16 100644
--- a/libraries/model-networking/src/model-networking/ModelCache.h
+++ b/libraries/model-networking/src/model-networking/ModelCache.h
@@ -26,6 +26,9 @@ class MeshPart;
 
 class GeometryMappingResource;
 
+using GeometryMappingPair = std::pair;
+Q_DECLARE_METATYPE(GeometryMappingPair)
+
 class Geometry {
 public:
     using Pointer = std::shared_ptr;
@@ -145,11 +148,13 @@ class ModelCache : public ResourceCache, public Dependency {
 public:
 
     GeometryResource::Pointer getGeometryResource(const QUrl& url,
-                                                  const QVariantHash& mapping = QVariantHash(),
+                                                  const GeometryMappingPair& mapping =
+                                                        GeometryMappingPair(QUrl(), QVariantHash()),
                                                   const QUrl& textureBaseUrl = QUrl());
 
     GeometryResource::Pointer getCollisionGeometryResource(const QUrl& url,
-                                                           const QVariantHash& mapping = QVariantHash(),
+                                                           const GeometryMappingPair& mapping =
+                                                                 GeometryMappingPair(QUrl(), QVariantHash()),
                                                            const QUrl& textureBaseUrl = QUrl());
 
 protected:
diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp
index 9145b4a79e..f4221e3d49 100644
--- a/libraries/networking/src/AddressManager.cpp
+++ b/libraries/networking/src/AddressManager.cpp
@@ -315,7 +315,9 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) {
 
                 // wasn't an address - lookup the place name
                 // we may have a path that defines a relative viewpoint - pass that through the lookup so we can go to it after
-                attemptPlaceNameLookup(lookupUrl.host(), lookupUrl.path(), trigger);
+                if (!lookupUrl.host().isNull() && !lookupUrl.host().isEmpty()) {
+                    attemptPlaceNameLookup(lookupUrl.host(), lookupUrl.path(), trigger);
+                }
             }
         }
 
@@ -337,7 +339,7 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) {
         // be loaded over http(s)
         // lookupUrl.scheme() == URL_SCHEME_HTTP ||
         // lookupUrl.scheme() == HIFI_URL_SCHEME_HTTPS ||
-        // TODO once a file can return a connection refusal if there were to be some kind of load error, we'd 
+        // TODO once a file can return a connection refusal if there were to be some kind of load error, we'd
         // need to store the previous domain tried in _lastVisitedURL. For now , do not store it.
 
         _previousAPILookup.clear();
diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp
index 8b9e37569c..a9dbc12b09 100644
--- a/libraries/networking/src/LimitedNodeList.cpp
+++ b/libraries/networking/src/LimitedNodeList.cpp
@@ -40,6 +40,9 @@
 
 static Setting::Handle LIMITED_NODELIST_LOCAL_PORT("LimitedNodeList.LocalPort", 0);
 
+using namespace std::chrono_literals;
+static const std::chrono::milliseconds CONNECTION_RATE_INTERVAL_MS = 1s;
+
 const std::set SOLO_NODE_TYPES = {
     NodeType::AvatarMixer,
     NodeType::AudioMixer,
@@ -88,6 +91,11 @@ LimitedNodeList::LimitedNodeList(int socketListenPort, int dtlsListenPort) :
     connect(statsSampleTimer, &QTimer::timeout, this, &LimitedNodeList::sampleConnectionStats);
     statsSampleTimer->start(CONNECTION_STATS_SAMPLE_INTERVAL_MSECS);
 
+    // Flush delayed adds every second
+    QTimer* delayedAddsFlushTimer = new QTimer(this);
+    connect(delayedAddsFlushTimer, &QTimer::timeout, this, &NodeList::processDelayedAdds);
+    delayedAddsFlushTimer->start(CONNECTION_RATE_INTERVAL_MS.count());
+
     // check the local socket right now
     updateLocalSocket();
 
@@ -367,7 +375,7 @@ bool LimitedNodeList::packetSourceAndHashMatchAndTrackBandwidth(const udt::Packe
 
             return true;
 
-        } else {
+        } else if (!isDelayedNode(sourceID)){
             HIFI_FCDEBUG(networking(),
                 "Packet of type" << headerType << "received from unknown node with Local ID" << sourceLocalID);
         }
@@ -558,25 +566,23 @@ SharedNodePointer LimitedNodeList::nodeWithLocalID(Node::LocalID localID) const
 }
 
 void LimitedNodeList::eraseAllNodes() {
-    QSet killedNodes;
+    std::vector killedNodes;
 
     {
         // iterate the current nodes - grab them so we can emit that they are dying
         // and then remove them from the hash
         QWriteLocker writeLocker(&_nodeMutex);
 
-        _localIDMap.clear();
-
         if (_nodeHash.size() > 0) {
             qCDebug(networking) << "LimitedNodeList::eraseAllNodes() removing all nodes from NodeList.";
 
-            auto it = _nodeHash.begin();
-
-            while (it != _nodeHash.end())  {
-                killedNodes.insert(it->second);
-                it = _nodeHash.unsafe_erase(it);
+            killedNodes.reserve(_nodeHash.size());
+            for (auto& pair : _nodeHash) {
+                killedNodes.push_back(pair.second);
             }
         }
+        _localIDMap.clear();
+        _nodeHash.clear();
     }
 
     foreach(const SharedNodePointer& killedNode, killedNodes) {
@@ -593,18 +599,13 @@ void LimitedNodeList::reset() {
 }
 
 bool LimitedNodeList::killNodeWithUUID(const QUuid& nodeUUID, ConnectionID newConnectionID) {
-    QReadLocker readLocker(&_nodeMutex);
-
-    NodeHash::iterator it = _nodeHash.find(nodeUUID);
-    if (it != _nodeHash.end()) {
-        SharedNodePointer matchingNode = it->second;
-
-        readLocker.unlock();
+    auto matchingNode = nodeWithUUID(nodeUUID);
 
+    if (matchingNode) {
         {
             QWriteLocker writeLocker(&_nodeMutex);
             _localIDMap.unsafe_erase(matchingNode->getLocalID());
-            _nodeHash.unsafe_erase(it);
+            _nodeHash.unsafe_erase(matchingNode->getUUID());
         }
 
         handleNodeKill(matchingNode, newConnectionID);
@@ -645,12 +646,8 @@ SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t
                                                    const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket,
                                                    Node::LocalID localID, bool isReplicated, bool isUpstream,
                                                    const QUuid& connectionSecret, const NodePermissions& permissions) {
-    QReadLocker readLocker(&_nodeMutex);
-    NodeHash::const_iterator it = _nodeHash.find(uuid);
-
-    if (it != _nodeHash.end()) {
-        SharedNodePointer& matchingNode = it->second;
-
+    auto matchingNode = nodeWithUUID(uuid);
+    if (matchingNode) {
         matchingNode->setPublicSocket(publicSocket);
         matchingNode->setLocalSocket(localSocket);
         matchingNode->setPermissions(permissions);
@@ -660,88 +657,127 @@ SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t
         matchingNode->setLocalID(localID);
 
         return matchingNode;
-    } else {
-        auto it = _connectionIDs.find(uuid);
-        if (it == _connectionIDs.end()) {
-            _connectionIDs[uuid] = INITIAL_CONNECTION_ID;
-        }
-
-        // we didn't have this node, so add them
-        Node* newNode = new Node(uuid, nodeType, publicSocket, localSocket);
-        newNode->setIsReplicated(isReplicated);
-        newNode->setIsUpstream(isUpstream || NodeType::isUpstream(nodeType));
-        newNode->setConnectionSecret(connectionSecret);
-        newNode->setPermissions(permissions);
-        newNode->setLocalID(localID);
-
-        // move the newly constructed node to the LNL thread
-        newNode->moveToThread(thread());
-
-        if (nodeType == NodeType::AudioMixer) {
-            LimitedNodeList::flagTimeForConnectionStep(LimitedNodeList::AddedAudioMixer);
-        }
-
-        SharedNodePointer newNodePointer(newNode, &QObject::deleteLater);
-
-        // if this is a solo node type, we assume that the DS has replaced its assignment and we should kill the previous node
-        if (SOLO_NODE_TYPES.count(newNode->getType())) {
-            // while we still have the read lock, see if there is a previous solo node we'll need to remove
-            auto previousSoloIt = std::find_if(_nodeHash.cbegin(), _nodeHash.cend(), [newNode](const UUIDNodePair& nodePair){
-                return nodePair.second->getType() == newNode->getType();
-            });
-
-            if (previousSoloIt != _nodeHash.cend()) {
-                // we have a previous solo node, switch to a write lock so we can remove it
-                readLocker.unlock();
-
-                QWriteLocker writeLocker(&_nodeMutex);
-
-                auto oldSoloNode = previousSoloIt->second;
-
-                _localIDMap.unsafe_erase(oldSoloNode->getLocalID());
-                _nodeHash.unsafe_erase(previousSoloIt);
-                handleNodeKill(oldSoloNode);
-
-                // convert the current lock back to a read lock for insertion of new node
-                writeLocker.unlock();
-                readLocker.relock();
-            }
-        }
-
-        // insert the new node and release our read lock
-#if defined(Q_OS_ANDROID) || (defined(__clang__) && defined(Q_OS_LINUX))
-        _nodeHash.insert(UUIDNodePair(newNode->getUUID(), newNodePointer));
-        _localIDMap.insert(std::pair(localID, newNodePointer));
-#else
-        _nodeHash.emplace(newNode->getUUID(), newNodePointer);
-        _localIDMap.emplace(localID, newNodePointer);
-#endif
-        readLocker.unlock();
-
-        qCDebug(networking) << "Added" << *newNode;
-
-        auto weakPtr = newNodePointer.toWeakRef(); // We don't want the lambdas to hold a strong ref
-
-        emit nodeAdded(newNodePointer);
-        if (newNodePointer->getActiveSocket()) {
-            emit nodeActivated(newNodePointer);
-        } else {
-            connect(newNodePointer.data(), &NetworkPeer::socketActivated, this, [this, weakPtr] {
-                auto sharedPtr = weakPtr.lock();
-                if (sharedPtr) {
-                    emit nodeActivated(sharedPtr);
-                    disconnect(sharedPtr.data(), &NetworkPeer::socketActivated, this, 0);
-                }
-            });
-        }
-
-        // Signal when a socket changes, so we can start the hole punch over.
-        connect(newNodePointer.data(), &NetworkPeer::socketUpdated, this, [this, weakPtr] {
-            emit nodeSocketUpdated(weakPtr);
-        });
-
-        return newNodePointer;
     }
+
+    auto removeOldNode = [&](auto node) {
+        if (node) {
+            {
+                QWriteLocker writeLocker(&_nodeMutex);
+                _localIDMap.unsafe_erase(node->getLocalID());
+                _nodeHash.unsafe_erase(node->getUUID());
+            }
+            handleNodeKill(node);
+        }
+    };
+
+    // if this is a solo node type, we assume that the DS has replaced its assignment and we should kill the previous node
+    if (SOLO_NODE_TYPES.count(nodeType)) {
+        removeOldNode(soloNodeOfType(nodeType));
+    }
+    // If there is a new node with the same socket, this is a reconnection, kill the old node
+    removeOldNode(findNodeWithAddr(publicSocket));
+    removeOldNode(findNodeWithAddr(localSocket));
+
+    auto it = _connectionIDs.find(uuid);
+    if (it == _connectionIDs.end()) {
+        _connectionIDs[uuid] = INITIAL_CONNECTION_ID;
+    }
+
+    // we didn't have this node, so add them
+    Node* newNode = new Node(uuid, nodeType, publicSocket, localSocket);
+    newNode->setIsReplicated(isReplicated);
+    newNode->setIsUpstream(isUpstream || NodeType::isUpstream(nodeType));
+    newNode->setConnectionSecret(connectionSecret);
+    newNode->setPermissions(permissions);
+    newNode->setLocalID(localID);
+
+    // move the newly constructed node to the LNL thread
+    newNode->moveToThread(thread());
+
+    if (nodeType == NodeType::AudioMixer) {
+        LimitedNodeList::flagTimeForConnectionStep(LimitedNodeList::AddedAudioMixer);
+    }
+
+    SharedNodePointer newNodePointer(newNode, &QObject::deleteLater);
+
+
+    {
+        QReadLocker readLocker(&_nodeMutex);
+        // insert the new node and release our read lock
+        _nodeHash.insert({ newNode->getUUID(), newNodePointer });
+        _localIDMap.insert({ localID, newNodePointer });
+    }
+
+    qCDebug(networking) << "Added" << *newNode;
+
+    auto weakPtr = newNodePointer.toWeakRef(); // We don't want the lambdas to hold a strong ref
+
+    emit nodeAdded(newNodePointer);
+    if (newNodePointer->getActiveSocket()) {
+        emit nodeActivated(newNodePointer);
+    } else {
+        connect(newNodePointer.data(), &NetworkPeer::socketActivated, this, [this, weakPtr] {
+            auto sharedPtr = weakPtr.lock();
+            if (sharedPtr) {
+                emit nodeActivated(sharedPtr);
+                disconnect(sharedPtr.data(), &NetworkPeer::socketActivated, this, 0);
+            }
+        });
+    }
+
+    // Signal when a socket changes, so we can start the hole punch over.
+    connect(newNodePointer.data(), &NetworkPeer::socketUpdated, this, [this, weakPtr] {
+        emit nodeSocketUpdated(weakPtr);
+    });
+
+    return newNodePointer;
+}
+
+void LimitedNodeList::addNewNode(NewNodeInfo info) {
+    // Throttle connection of new agents.
+    if (info.type == NodeType::Agent && _nodesAddedInCurrentTimeSlice >= _maxConnectionRate) {
+        delayNodeAdd(info);
+        return;
+    }
+
+    SharedNodePointer node = addOrUpdateNode(info.uuid, info.type, info.publicSocket, info.localSocket,
+                                             info.sessionLocalID, info.isReplicated, false,
+                                             info.connectionSecretUUID, info.permissions);
+
+    ++_nodesAddedInCurrentTimeSlice;
+}
+
+void LimitedNodeList::delayNodeAdd(NewNodeInfo info) {
+    _delayedNodeAdds.push_back(info);
+}
+
+void LimitedNodeList::removeDelayedAdd(QUuid nodeUUID) {
+    auto it = std::find_if(_delayedNodeAdds.begin(), _delayedNodeAdds.end(), [&](auto info) {
+        return info.uuid == nodeUUID;
+    });
+    if (it != _delayedNodeAdds.end()) {
+        _delayedNodeAdds.erase(it);
+    }
+}
+
+bool LimitedNodeList::isDelayedNode(QUuid nodeUUID) {
+    auto it = std::find_if(_delayedNodeAdds.begin(), _delayedNodeAdds.end(), [&](auto info) {
+        return info.uuid == nodeUUID;
+    });
+    return it != _delayedNodeAdds.end();
+}
+
+void LimitedNodeList::processDelayedAdds() {
+    _nodesAddedInCurrentTimeSlice = 0;
+
+    auto nodesToAdd = glm::min(_delayedNodeAdds.size(), _maxConnectionRate);
+    auto firstNodeToAdd = _delayedNodeAdds.begin();
+    auto lastNodeToAdd = firstNodeToAdd + nodesToAdd;
+
+    for (auto it = firstNodeToAdd; it != lastNodeToAdd; ++it) {
+        addNewNode(*it);
+    }
+    _delayedNodeAdds.erase(firstNodeToAdd, lastNodeToAdd);
 }
 
 std::unique_ptr LimitedNodeList::constructPingPacket(const QUuid& nodeId, PingType_t pingType) {
@@ -801,13 +837,13 @@ unsigned int LimitedNodeList::broadcastToNodes(std::unique_ptr packet,
 
     eachNode([&](const SharedNodePointer& node){
         if (node && destinationNodeTypes.contains(node->getType())) {
-			if (packet->isReliable()) {
-				auto packetCopy = NLPacket::createCopy(*packet);
-				sendPacket(std::move(packetCopy), *node);
-			} else {
-				sendUnreliablePacket(*packet, *node);
-			}
-			++n;
+            if (packet->isReliable()) {
+                auto packetCopy = NLPacket::createCopy(*packet);
+                sendPacket(std::move(packetCopy), *node);
+            } else {
+                sendUnreliablePacket(*packet, *node);
+            }
+            ++n;
         }
     });
 
diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h
index 450fad96a9..8593ad4b1b 100644
--- a/libraries/networking/src/LimitedNodeList.h
+++ b/libraries/networking/src/LimitedNodeList.h
@@ -51,6 +51,8 @@ const int INVALID_PORT = -1;
 
 const quint64 NODE_SILENCE_THRESHOLD_MSECS = 5 * 1000;
 
+static const size_t DEFAULT_MAX_CONNECTION_RATE { std::numeric_limits::max() };
+
 extern const std::set SOLO_NODE_TYPES;
 
 const char DEFAULT_ASSIGNMENT_SERVER_HOSTNAME[] = "localhost";
@@ -205,7 +207,10 @@ public:
                     int* lockWaitOut = nullptr,
                     int* nodeTransformOut = nullptr,
                     int* functorOut = nullptr) {
-        auto start = usecTimestampNow();
+        quint64 start, endTransform, endFunctor;
+
+        start = usecTimestampNow();
+        std::vector nodes;
         {
             QReadLocker readLock(&_nodeMutex);
             auto endLock = usecTimestampNow();
@@ -216,21 +221,21 @@ public:
             // Size of _nodeHash could change at any time,
             // so reserve enough memory for the current size
             // and then back insert all the nodes found
-            std::vector nodes;
             nodes.reserve(_nodeHash.size());
             std::transform(_nodeHash.cbegin(), _nodeHash.cend(), std::back_inserter(nodes), [&](const NodeHash::value_type& it) {
                 return it.second;
             });
-            auto endTransform = usecTimestampNow();
+
+            endTransform = usecTimestampNow();
             if (nodeTransformOut) {
                 *nodeTransformOut = (endTransform - endLock);
             }
+        }
 
-            functor(nodes.cbegin(), nodes.cend());
-            auto endFunctor = usecTimestampNow();
-            if (functorOut) {
-                *functorOut = (endFunctor - endTransform);
-            }
+        functor(nodes.cbegin(), nodes.cend());
+        endFunctor = usecTimestampNow();
+        if (functorOut) {
+            *functorOut = (endFunctor - endTransform);
         }
     }
 
@@ -316,6 +321,9 @@ public:
     void sendFakedHandshakeRequestToNode(SharedNodePointer node);
 #endif
 
+    size_t getMaxConnectionRate() const { return _maxConnectionRate; }
+    void setMaxConnectionRate(size_t rate) { _maxConnectionRate = rate; }
+
     int getInboundPPS() const { return _inboundPPS; }
     int getOutboundPPS() const { return _outboundPPS; }
     float getInboundKbps() const { return _inboundKbps; }
@@ -367,7 +375,20 @@ protected slots:
 
     void clientConnectionToSockAddrReset(const HifiSockAddr& sockAddr);
 
+    void processDelayedAdds();
+
 protected:
+    struct NewNodeInfo {
+        qint8 type;
+        QUuid uuid;
+        HifiSockAddr publicSocket;
+        HifiSockAddr localSocket;
+        NodePermissions permissions;
+        bool isReplicated;
+        Node::LocalID sessionLocalID;
+        QUuid connectionSecretUUID;
+    };
+
     LimitedNodeList(int socketListenPort = INVALID_PORT, int dtlsListenPort = INVALID_PORT);
     LimitedNodeList(LimitedNodeList const&) = delete; // Don't implement, needed to avoid copies of singleton
     void operator=(LimitedNodeList const&) = delete; // Don't implement, needed to avoid copies of singleton
@@ -390,6 +411,11 @@ protected:
 
     bool sockAddrBelongsToNode(const HifiSockAddr& sockAddr);
 
+    void addNewNode(NewNodeInfo info);
+    void delayNodeAdd(NewNodeInfo info);
+    void removeDelayedAdd(QUuid nodeUUID);
+    bool isDelayedNode(QUuid nodeUUID);
+
     NodeHash _nodeHash;
     mutable QReadWriteLock _nodeMutex { QReadWriteLock::Recursive };
     udt::Socket _nodeSocket;
@@ -440,6 +466,10 @@ private:
     Node::LocalID _sessionLocalID { 0 };
     bool _flagTimeForConnectionStep { false }; // only keep track in interface
 
+    size_t _maxConnectionRate { DEFAULT_MAX_CONNECTION_RATE };
+    size_t _nodesAddedInCurrentTimeSlice { 0 };
+    std::vector _delayedNodeAdds;
+
     int _inboundPPS { 0 };
     int _outboundPPS { 0 };
     float _inboundKbps { 0.0f };
diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp
index 5e8909db2b..e6eb6087b0 100644
--- a/libraries/networking/src/NodeList.cpp
+++ b/libraries/networking/src/NodeList.cpp
@@ -200,7 +200,6 @@ void NodeList::timePingReply(ReceivedMessage& message, const SharedNodePointer&
 }
 
 void NodeList::processPingPacket(QSharedPointer message, SharedNodePointer sendingNode) {
-
     // send back a reply
     auto replyPacket = constructPingReplyPacket(*message);
     const HifiSockAddr& senderSockAddr = message->getSenderSockAddr();
@@ -291,41 +290,47 @@ void NodeList::addSetOfNodeTypesToNodeInterestSet(const NodeSet& setOfNodeTypes)
 
 void NodeList::sendDomainServerCheckIn() {
 
+    // This function is called by the server check-in timer thread
+    // not the NodeList thread.  Calling it on the NodeList thread
+    // resulted in starvation of the server check-in function.
+    // be VERY CAREFUL modifying this code as members of NodeList
+    // may be called by multiple threads.
+
     if (!_sendDomainServerCheckInEnabled) {
         qCDebug(networking) << "Refusing to send a domain-server check in while it is disabled.";
         return;
     }
 
-    if (thread() != QThread::currentThread()) {
-        QMetaObject::invokeMethod(this, "sendDomainServerCheckIn", Qt::QueuedConnection);
-        return;
-    }
-
     if (_isShuttingDown) {
         qCDebug(networking) << "Refusing to send a domain-server check in while shutting down.";
         return;
     }
 
-    if (_publicSockAddr.isNull()) {
+    auto publicSockAddr = _publicSockAddr;
+    auto domainHandlerIp = _domainHandler.getIP();
+
+    if (publicSockAddr.isNull()) {
         // we don't know our public socket and we need to send it to the domain server
         qCDebug(networking) << "Waiting for inital public socket from STUN. Will not send domain-server check in.";
-    } else if (_domainHandler.getIP().isNull() && _domainHandler.requiresICE()) {
+    } else if (domainHandlerIp.isNull() && _domainHandler.requiresICE()) {
         qCDebug(networking) << "Waiting for ICE discovered domain-server socket. Will not send domain-server check in.";
         handleICEConnectionToDomainServer();
         // let the domain handler know we are due to send a checkin packet
-    } else if (!_domainHandler.getIP().isNull() && !_domainHandler.checkInPacketTimeout()) {
-
-        PacketType domainPacketType = !_domainHandler.isConnected()
+    } else if (!domainHandlerIp.isNull() && !_domainHandler.checkInPacketTimeout()) {
+        bool domainIsConnected = _domainHandler.isConnected();
+        HifiSockAddr domainSockAddr = _domainHandler.getSockAddr();
+        PacketType domainPacketType = !domainIsConnected
             ? PacketType::DomainConnectRequest : PacketType::DomainListRequest;
 
-        if (!_domainHandler.isConnected()) {
-            qCDebug(networking) << "Sending connect request to domain-server at" << _domainHandler.getHostname();
+        if (!domainIsConnected) {
+            auto hostname = _domainHandler.getHostname();
+            qCDebug(networking) << "Sending connect request to domain-server at" << hostname;
 
             // is this our localhost domain-server?
             // if so we need to make sure we have an up-to-date local port in case it restarted
 
-            if (_domainHandler.getSockAddr().getAddress() == QHostAddress::LocalHost
-                || _domainHandler.getHostname() == "localhost") {
+            if (domainSockAddr.getAddress() == QHostAddress::LocalHost
+                || hostname == "localhost") {
 
                 quint16 domainPort = DEFAULT_DOMAIN_SERVER_PORT;
                 getLocalServerPortFromSharedMemory(DOMAIN_SERVER_LOCAL_PORT_SMEM_KEY, domainPort);
@@ -338,7 +343,7 @@ void NodeList::sendDomainServerCheckIn() {
         auto accountManager = DependencyManager::get();
         const QUuid& connectionToken = _domainHandler.getConnectionToken();
 
-        bool requiresUsernameSignature = !_domainHandler.isConnected() && !connectionToken.isNull();
+        bool requiresUsernameSignature = !domainIsConnected && !connectionToken.isNull();
 
         if (requiresUsernameSignature && !accountManager->getAccountInfo().hasPrivateKey()) {
             qWarning() << "A keypair is required to present a username signature to the domain-server"
@@ -353,6 +358,7 @@ void NodeList::sendDomainServerCheckIn() {
 
         QDataStream packetStream(domainPacket.get());
 
+        HifiSockAddr localSockAddr = _localSockAddr;
         if (domainPacketType == PacketType::DomainConnectRequest) {
 
 #if (PR_BUILD || DEV_BUILD)
@@ -361,13 +367,9 @@ void NodeList::sendDomainServerCheckIn() {
             }
 #endif
 
-            QUuid connectUUID;
+            QUuid connectUUID = _domainHandler.getAssignmentUUID();
 
-            if (!_domainHandler.getAssignmentUUID().isNull()) {
-                // this is a connect request and we're an assigned node
-                // so set our packetUUID as the assignment UUID
-                connectUUID = _domainHandler.getAssignmentUUID();
-            } else if (_domainHandler.requiresICE()) {
+            if (connectUUID.isNull() && _domainHandler.requiresICE()) {
                 // this is a connect request and we're an interface client
                 // that used ice to discover the DS
                 // so send our ICE client UUID with the connect request
@@ -383,10 +385,9 @@ void NodeList::sendDomainServerCheckIn() {
 
             // if possible, include the MAC address for the current interface in our connect request
             QString hardwareAddress;
-
             for (auto networkInterface : QNetworkInterface::allInterfaces()) {
                 for (auto interfaceAddress : networkInterface.addressEntries()) {
-                    if (interfaceAddress.ip() == _localSockAddr.getAddress()) {
+                    if (interfaceAddress.ip() == localSockAddr.getAddress()) {
                         // this is the interface whose local IP matches what we've detected the current IP to be
                         hardwareAddress = networkInterface.hardwareAddress();
 
@@ -410,10 +411,10 @@ void NodeList::sendDomainServerCheckIn() {
 
         // pack our data to send to the domain-server including
         // the hostname information (so the domain-server can see which place name we came in on)
-        packetStream << _ownerType.load() << _publicSockAddr << _localSockAddr << _nodeTypesOfInterest.toList();
+        packetStream << _ownerType.load() << publicSockAddr << localSockAddr << _nodeTypesOfInterest.toList();
         packetStream << DependencyManager::get()->getPlaceName();
 
-        if (!_domainHandler.isConnected()) {
+        if (!domainIsConnected) {
             DataServerAccountInfo& accountInfo = accountManager->getAccountInfo();
             packetStream << accountInfo.getUsername();
 
@@ -433,9 +434,9 @@ void NodeList::sendDomainServerCheckIn() {
         checkinCount = std::min(checkinCount, MAX_CHECKINS_TOGETHER);
         for (int i = 1; i < checkinCount; ++i) {
             auto packetCopy = domainPacket->createCopy(*domainPacket);
-            sendPacket(std::move(packetCopy), _domainHandler.getSockAddr());
+            sendPacket(std::move(packetCopy), domainSockAddr);
         }
-        sendPacket(std::move(domainPacket), _domainHandler.getSockAddr());
+        sendPacket(std::move(domainPacket), domainSockAddr);
         
     }
 }
@@ -708,37 +709,28 @@ void NodeList::processDomainServerRemovedNode(QSharedPointer me
     QUuid nodeUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
     qCDebug(networking) << "Received packet from domain-server to remove node with UUID" << uuidStringWithoutCurlyBraces(nodeUUID);
     killNodeWithUUID(nodeUUID);
+    removeDelayedAdd(nodeUUID);
 }
 
 void NodeList::parseNodeFromPacketStream(QDataStream& packetStream) {
-    // setup variables to read into from QDataStream
-    qint8 nodeType;
-    QUuid nodeUUID, connectionSecretUUID;
-    HifiSockAddr nodePublicSocket, nodeLocalSocket;
-    NodePermissions permissions;
-    bool isReplicated;
-    Node::LocalID sessionLocalID;
+    NewNodeInfo info;
 
-    packetStream >> nodeType >> nodeUUID >> nodePublicSocket >> nodeLocalSocket >> permissions
-        >> isReplicated >> sessionLocalID;
+    packetStream >> info.type
+                 >> info.uuid
+                 >> info.publicSocket
+                 >> info.localSocket
+                 >> info.permissions
+                 >> info.isReplicated
+                 >> info.sessionLocalID
+                 >> info.connectionSecretUUID;
 
     // if the public socket address is 0 then it's reachable at the same IP
     // as the domain server
-    if (nodePublicSocket.getAddress().isNull()) {
-        nodePublicSocket.setAddress(_domainHandler.getIP());
+    if (info.publicSocket.getAddress().isNull()) {
+        info.publicSocket.setAddress(_domainHandler.getIP());
     }
 
-    packetStream >> connectionSecretUUID;
-
-    SharedNodePointer node = addOrUpdateNode(nodeUUID, nodeType, nodePublicSocket, nodeLocalSocket,
-                                             sessionLocalID, isReplicated, false, connectionSecretUUID, permissions);
-
-    // nodes that are downstream or upstream of our own type are kept alive when we hear about them from the domain server
-    // and always have their public socket as their active socket
-    if (node->getType() == NodeType::downstreamType(_ownerType) || node->getType() == NodeType::upstreamType(_ownerType)) {
-        node->setLastHeardMicrostamp(usecTimestampNow());
-        node->activatePublicSocket();
-    }
+    addNewNode(info);
 }
 
 void NodeList::sendAssignment(Assignment& assignment) {
@@ -785,7 +777,6 @@ void NodeList::pingPunchForInactiveNode(const SharedNodePointer& node) {
 }
 
 void NodeList::startNodeHolePunch(const SharedNodePointer& node) {
-
     // we don't hole punch to downstream servers, since it is assumed that we have a direct line to them
     // we also don't hole punch to relayed upstream nodes, since we do not communicate directly with them
 
@@ -799,6 +790,14 @@ void NodeList::startNodeHolePunch(const SharedNodePointer& node) {
         // ping this node immediately
         pingPunchForInactiveNode(node);
     }
+
+    // nodes that are downstream or upstream of our own type are kept alive when we hear about them from the domain server
+    // and always have their public socket as their active socket
+    if (node->getType() == NodeType::downstreamType(_ownerType) || node->getType() == NodeType::upstreamType(_ownerType)) {
+        node->setLastHeardMicrostamp(usecTimestampNow());
+        node->activatePublicSocket();
+    }
+
 }
 
 void NodeList::handleNodePingTimeout() {
diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp
index 8ad1b41020..d5abb27a27 100644
--- a/libraries/networking/src/ResourceCache.cpp
+++ b/libraries/networking/src/ResourceCache.cpp
@@ -353,16 +353,19 @@ QSharedPointer ResourceCache::getResource(const QUrl& url, const QUrl&
             // We've seen this extra info before
             resource = resourcesWithExtraHashIter.value().lock();
         } else if (resourcesWithExtraHash.size() > 0.0f) {
-            // We haven't seen this extra info before, but we've already downloaded the resource.  We need a new copy of this object (with any old hash).
-            resource = createResourceCopy(resourcesWithExtraHash.begin().value().lock());
-            resource->setExtra(extra, true);
-            resource->setExtraHash(extraHash);
-            resource->setSelf(resource);
-            resource->setCache(this);
-            resource->moveToThread(qApp->thread());
-            connect(resource.data(), &Resource::updateSize, this, &ResourceCache::updateTotalSize);
-            resourcesWithExtraHash.insert(extraHash, resource);
-            resource->ensureLoading();
+            auto oldResource = resourcesWithExtraHash.begin().value().lock();
+            if (oldResource) {
+                // We haven't seen this extra info before, but we've already downloaded the resource.  We need a new copy of this object (with any old hash).
+                resource = createResourceCopy(oldResource);
+                resource->setExtra(extra);
+                resource->setExtraHash(extraHash);
+                resource->setSelf(resource);
+                resource->setCache(this);
+                resource->moveToThread(qApp->thread());
+                connect(resource.data(), &Resource::updateSize, this, &ResourceCache::updateTotalSize);
+                resourcesWithExtraHash.insert(extraHash, resource);
+                resource->ensureLoading();
+            }
         }
     }
     if (resource) {
@@ -375,7 +378,7 @@ QSharedPointer ResourceCache::getResource(const QUrl& url, const QUrl&
 
     if (!resource) {
         resource = createResource(url);
-        resource->setExtra(extra, false);
+        resource->setExtra(extra);
         resource->setExtraHash(extraHash);
         resource->setSelf(resource);
         resource->setCache(this);
diff --git a/libraries/networking/src/ResourceCache.h b/libraries/networking/src/ResourceCache.h
index 62800a6ac2..2096213273 100644
--- a/libraries/networking/src/ResourceCache.h
+++ b/libraries/networking/src/ResourceCache.h
@@ -417,7 +417,7 @@ public:
     unsigned int getDownloadAttempts() { return _attempts; }
     unsigned int getDownloadAttemptsRemaining() { return _attemptsRemaining; }
 
-    virtual void setExtra(void* extra, bool isNewExtra) {};
+    virtual void setExtra(void* extra) {};
     void setExtraHash(size_t extraHash) { _extraHash = extraHash; }
     size_t getExtraHash() const { return _extraHash; }
 
diff --git a/libraries/networking/src/ThreadedAssignment.cpp b/libraries/networking/src/ThreadedAssignment.cpp
index bdba47f0ed..9b9a53b469 100644
--- a/libraries/networking/src/ThreadedAssignment.cpp
+++ b/libraries/networking/src/ThreadedAssignment.cpp
@@ -102,6 +102,11 @@ void ThreadedAssignment::addPacketStatsAndSendStatsPacket(QJsonObject statsObjec
 
     statsObject["io_stats"] = ioStats;
 
+    QJsonObject assignmentStats;
+    assignmentStats["numQueuedCheckIns"] = _numQueuedCheckIns;
+
+    statsObject["assignmentStats"] = assignmentStats;
+
     nodeList->sendStatsToDomainServer(statsObject);
 }
 
@@ -119,10 +124,16 @@ void ThreadedAssignment::checkInWithDomainServerOrExit() {
         stop();
     } else {
         auto nodeList = DependencyManager::get();
-        QMetaObject::invokeMethod(nodeList.data(), "sendDomainServerCheckIn");
+        // Call sendDomainServerCheckIn directly instead of putting it on
+        // the event queue.  Under high load, the event queue can back up
+        // longer than the total timeout period and cause a restart
+        nodeList->sendDomainServerCheckIn();
 
         // increase the number of queued check ins
         _numQueuedCheckIns++;
+        if (_numQueuedCheckIns > 1) {
+            qCDebug(networking) << "Number of queued checkins = " << _numQueuedCheckIns;
+        }
     }
 }
 
diff --git a/libraries/networking/src/udt/Connection.cpp b/libraries/networking/src/udt/Connection.cpp
index 7ab2296935..418dc8f417 100644
--- a/libraries/networking/src/udt/Connection.cpp
+++ b/libraries/networking/src/udt/Connection.cpp
@@ -31,7 +31,6 @@ using namespace udt;
 using namespace std::chrono;
 
 Connection::Connection(Socket* parentSocket, HifiSockAddr destination, std::unique_ptr congestionControl) :
-    QObject(parentSocket),
     _parentSocket(parentSocket),
     _destination(destination),
     _congestionControl(move(congestionControl))
diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h
index 5f55c189ce..1489f8e16c 100644
--- a/libraries/networking/src/udt/PacketHeaders.h
+++ b/libraries/networking/src/udt/PacketHeaders.h
@@ -260,8 +260,11 @@ enum class EntityVersion : PacketVersion {
     MissingWebEntityProperties,
     PulseProperties,
     RingGizmoEntities,
+    AvatarPriorityZone,
     ShowKeyboardFocusHighlight,
     WebBillboardMode,
+    ModelScale,
+    ReOrderParentIDProperties,
 
     // Add new versions above here
     NUM_PACKET_TYPE,
diff --git a/libraries/networking/src/udt/Socket.cpp b/libraries/networking/src/udt/Socket.cpp
index 358acce694..7829e3727c 100644
--- a/libraries/networking/src/udt/Socket.cpp
+++ b/libraries/networking/src/udt/Socket.cpp
@@ -251,7 +251,10 @@ Connection* Socket::findOrCreateConnection(const HifiSockAddr& sockAddr, bool fi
             auto congestionControl = _ccFactory->create();
             congestionControl->setMaxBandwidth(_maxBandwidth);
             auto connection = std::unique_ptr(new Connection(this, sockAddr, std::move(congestionControl)));
-
+            if (QThread::currentThread() != thread()) {
+                qCDebug(networking) << "Moving new Connection to NodeList thread";
+                connection->moveToThread(thread());
+            }
             // allow higher-level classes to find out when connections have completed a handshake
             QObject::connect(connection.get(), &Connection::receiverHandshakeRequestComplete,
                              this, &Socket::clientHandshakeRequestComplete);
diff --git a/libraries/octree/src/Octree.h b/libraries/octree/src/Octree.h
index aac29201f1..82076f618b 100644
--- a/libraries/octree/src/Octree.h
+++ b/libraries/octree/src/Octree.h
@@ -149,7 +149,7 @@ public:
 
     OctreeElementPointer getRoot() { return _rootElement; }
 
-    virtual void eraseNonLocalEntities() { _isDirty = true; };
+    virtual void eraseDomainAndNonOwnedEntities() { _isDirty = true; };
     virtual void eraseAllOctreeElements(bool createNewRoot = true);
 
     virtual void readBitstreamToTree(const unsigned char* bitstream,  uint64_t bufferSizeBytes, ReadBitstreamToTreeParams& args);
diff --git a/libraries/octree/src/OctreeProcessor.cpp b/libraries/octree/src/OctreeProcessor.cpp
index 18c8630391..03c8b9ca2f 100644
--- a/libraries/octree/src/OctreeProcessor.cpp
+++ b/libraries/octree/src/OctreeProcessor.cpp
@@ -198,10 +198,10 @@ void OctreeProcessor::processDatagram(ReceivedMessage& message, SharedNodePointe
 }
 
 
-void OctreeProcessor::clearNonLocalEntities() {
+void OctreeProcessor::clearDomainAndNonOwnedEntities() {
     if (_tree) {
         _tree->withWriteLock([&] {
-            _tree->eraseNonLocalEntities();
+            _tree->eraseDomainAndNonOwnedEntities();
         });
     }
 }
diff --git a/libraries/octree/src/OctreeProcessor.h b/libraries/octree/src/OctreeProcessor.h
index bc5618e657..40af7a39f8 100644
--- a/libraries/octree/src/OctreeProcessor.h
+++ b/libraries/octree/src/OctreeProcessor.h
@@ -43,7 +43,7 @@ public:
     virtual void init();
 
     /// clears the tree
-    virtual void clearNonLocalEntities();
+    virtual void clearDomainAndNonOwnedEntities();
     virtual void clear();
 
     float getAverageElementsPerPacket() const { return _elementsPerPacket.getAverage(); }
diff --git a/libraries/oculusMobile/src/ovr/Framebuffer.cpp b/libraries/oculusMobile/src/ovr/Framebuffer.cpp
index 4c4fd2a983..0f59eef614 100644
--- a/libraries/oculusMobile/src/ovr/Framebuffer.cpp
+++ b/libraries/oculusMobile/src/ovr/Framebuffer.cpp
@@ -32,18 +32,19 @@ void Framebuffer::create(const glm::uvec2& size) {
     _validTexture = false;
 
     // Depth renderbuffer
-    glGenRenderbuffers(1, &_depth);
+  /*  glGenRenderbuffers(1, &_depth);
     glBindRenderbuffer(GL_RENDERBUFFER, _depth);
     glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, _size.x, _size.y);
     glBindRenderbuffer(GL_RENDERBUFFER, 0);
-
+*/
     // Framebuffer
     glGenFramebuffers(1, &_fbo);
-    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo);
-    glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _depth);
-    glBindFramebuffer(GL_FRAMEBUFFER, 0);
+  //  glBindFramebuffer(GL_DRAW_FRAMEBUFFER, _fbo);
+  //  glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _depth);
+  //  glBindFramebuffer(GL_FRAMEBUFFER, 0);
 
     _swapChain = vrapi_CreateTextureSwapChain3(VRAPI_TEXTURE_TYPE_2D, GL_RGBA8, _size.x, _size.y, 1, 3);
+
     _length = vrapi_GetTextureSwapChainLength(_swapChain);
     if (!_length) {
         __android_log_write(ANDROID_LOG_WARN, "QQQ_OVR", "Unable to count swap chain textures");
diff --git a/libraries/oculusMobile/src/ovr/VrHandler.cpp b/libraries/oculusMobile/src/ovr/VrHandler.cpp
index de2b4e1ff6..3fe3901517 100644
--- a/libraries/oculusMobile/src/ovr/VrHandler.cpp
+++ b/libraries/oculusMobile/src/ovr/VrHandler.cpp
@@ -140,7 +140,11 @@ struct VrSurface : public TaskQueue {
         if (vrReady != vrRunning) {
             if (vrRunning) {
                 __android_log_write(ANDROID_LOG_WARN, "QQQ_OVR", "vrapi_LeaveVrMode");
+                vrapi_SetClockLevels(session, 1, 1);
+                vrapi_SetExtraLatencyMode(session, VRAPI_EXTRA_LATENCY_MODE_OFF);
+                vrapi_SetDisplayRefreshRate(session, 60);
                 vrapi_LeaveVrMode(session);
+
                 session = nullptr;
                 oculusActivity = nullptr;
             } else {
@@ -315,10 +319,6 @@ JNIEXPORT void JNICALL Java_io_highfidelity_oculus_OculusMobileActivity_nativeOn
     SURFACE.onCreate(env, obj);
 }
 
-JNIEXPORT void JNICALL Java_io_highfidelity_oculus_OculusMobileActivity_nativeOnDestroy(JNIEnv*, jclass) {
-    __android_log_write(ANDROID_LOG_WARN, "QQQ_JNI", __FUNCTION__);
-}
-
 JNIEXPORT void JNICALL Java_io_highfidelity_oculus_OculusMobileActivity_nativeOnResume(JNIEnv*, jclass) {
     __android_log_write(ANDROID_LOG_WARN, "QQQ_JNI", __FUNCTION__);
     SURFACE.setResumed(true);
diff --git a/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp
index 78d80443d8..9809d02866 100644
--- a/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp
+++ b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp
@@ -121,6 +121,7 @@ QRectF OculusMobileDisplayPlugin::getPlayAreaRect() {
 
 glm::mat4 OculusMobileDisplayPlugin::getEyeProjection(Eye eye, const glm::mat4& baseProjection) const {
     glm::mat4 result = baseProjection;
+
     VrHandler::withOvrMobile([&](ovrMobile* session){
         auto trackingState = vrapi_GetPredictedTracking2(session, 0.0);
         result = ovr::Fov{ trackingState.Eye[eye].ProjectionMatrix }.withZ(baseProjection);
@@ -130,15 +131,19 @@ glm::mat4 OculusMobileDisplayPlugin::getEyeProjection(Eye eye, const glm::mat4&
 
 glm::mat4 OculusMobileDisplayPlugin::getCullingProjection(const glm::mat4& baseProjection) const {
     glm::mat4 result = baseProjection;
+
     VrHandler::withOvrMobile([&](ovrMobile* session){
         auto trackingState = vrapi_GetPredictedTracking2(session, 0.0);
         ovr::Fov fovs[2];
         for (size_t i = 0; i < 2; ++i) {
             fovs[i].extract(trackingState.Eye[i].ProjectionMatrix);
         }
+
         fovs[0].extend(fovs[1]);
-        return fovs[0].withZ(baseProjection);
+        result= glm::scale( fovs[0].withZ(baseProjection),glm::vec3(1.5f));
+        return result;
     });
+
     return result;
 }
 
@@ -168,10 +173,8 @@ bool OculusMobileDisplayPlugin::isHmdMounted() const {
 static void goToDevMobile() {
     auto addressManager = DependencyManager::get();
     auto currentAddress = addressManager->currentAddress().toString().toStdString();
-    if (std::string::npos == currentAddress.find("dev-mobile")) {
-        addressManager->handleLookupString("hifi://dev-mobile/495.236,501.017,482.434/0,0.97452,0,-0.224301");
-        //addressManager->handleLookupString("hifi://dev-mobile/504,498,491/0,0,0,0");
-        //addressManager->handleLookupString("hifi://dev-mobile/0,-1,1");
+    if (std::string::npos == currentAddress.find("quest-dev")) {
+        addressManager->handleLookupString("hifi://quest-dev");
     }
 }
 
@@ -217,12 +220,12 @@ bool OculusMobileDisplayPlugin::beginFrameRender(uint32_t frameIndex) {
        });
     }
 
-    //  static uint32_t count = 0;
-    //  if ((++count % 1000) == 0) {
-    //      AbstractViewStateInterface::instance()->postLambdaEvent([] {
-    //          goToDevMobile();
-    //      });
-    //  }
+  //  static uint32_t count = 0;
+  //  if ((++count % 1000) == 0) {
+  //      AbstractViewStateInterface::instance()->postLambdaEvent([] {
+  //          goToDevMobile();
+  //      });
+  //  }
 
     return result && Parent::beginFrameRender(frameIndex);
 }
diff --git a/libraries/physics/src/CharacterController.cpp b/libraries/physics/src/CharacterController.cpp
index 66ce5f32bf..a5f1a0598f 100755
--- a/libraries/physics/src/CharacterController.cpp
+++ b/libraries/physics/src/CharacterController.cpp
@@ -781,18 +781,18 @@ void CharacterController::updateState() {
                 const float jumpSpeed = sqrtf(2.0f * -DEFAULT_AVATAR_GRAVITY * jumpHeight);
                 if ((velocity.dot(_currentUp) <= (jumpSpeed / 2.0f)) && ((_floorDistance < FLY_TO_GROUND_THRESHOLD) || _hasSupport)) {
                     SET_STATE(State::Ground, "hit ground");
-                } else if (_flyingAllowed) {
+                } else if (_zoneFlyingAllowed) {
                     btVector3 desiredVelocity = _targetVelocity;
                     if (desiredVelocity.length2() < MIN_TARGET_SPEED_SQUARED) {
                         desiredVelocity = btVector3(0.0f, 0.0f, 0.0f);
                     }
                     bool vertTargetSpeedIsNonZero = desiredVelocity.dot(_currentUp) > MIN_TARGET_SPEED;
-                    if ((jumpButtonHeld || vertTargetSpeedIsNonZero) && (_takeoffJumpButtonID != _jumpButtonDownCount)) {
+                    if (_comfortFlyingAllowed && (jumpButtonHeld || vertTargetSpeedIsNonZero) && (_takeoffJumpButtonID != _jumpButtonDownCount)) {
                         SET_STATE(State::Hover, "double jump button");
-                    } else if ((jumpButtonHeld || vertTargetSpeedIsNonZero) && (now - _jumpButtonDownStartTime) > JUMP_TO_HOVER_PERIOD) {
+                    } else if (_comfortFlyingAllowed && (jumpButtonHeld || vertTargetSpeedIsNonZero) && (now - _jumpButtonDownStartTime) > JUMP_TO_HOVER_PERIOD) {
                         SET_STATE(State::Hover, "jump button held");
-                    } else if (_floorDistance > _scaleFactor * DEFAULT_AVATAR_FALL_HEIGHT) {
-                        // Transition to hover if we are above the fall threshold
+                    } else if ((!rayHasHit && !_hasSupport) || _floorDistance > _scaleFactor * DEFAULT_AVATAR_FALL_HEIGHT) {
+                        // Transition to hover if there's no ground beneath us or we are above the fall threshold, regardless of _comfortFlyingAllowed
                         SET_STATE(State::Hover, "above fall threshold");
                     }
                 }
@@ -801,8 +801,10 @@ void CharacterController::updateState() {
             case State::Hover:
                 btScalar horizontalSpeed = (velocity - velocity.dot(_currentUp) * _currentUp).length();
                 bool flyingFast = horizontalSpeed > (MAX_WALKING_SPEED * 0.75f);
-                if (!_flyingAllowed) {
-                    SET_STATE(State::InAir, "flying not allowed");
+                if (!_zoneFlyingAllowed) {
+                    SET_STATE(State::InAir, "zone flying not allowed");
+                } else if (!_comfortFlyingAllowed && (rayHasHit || _hasSupport || _floorDistance < FLY_TO_GROUND_THRESHOLD)) {
+                    SET_STATE(State::InAir, "comfort flying not allowed");
                 } else if ((_floorDistance < MIN_HOVER_HEIGHT) && !jumpButtonHeld && !flyingFast) {
                     SET_STATE(State::InAir, "near ground");
                 } else if (((_floorDistance < FLY_TO_GROUND_THRESHOLD) || _hasSupport) && !flyingFast) {
@@ -847,12 +849,6 @@ bool CharacterController::getRigidBodyLocation(glm::vec3& avatarRigidBodyPositio
     return true;
 }
 
-void CharacterController::setFlyingAllowed(bool value) {
-    if (value != _flyingAllowed) {
-        _flyingAllowed = value;
-    }
-}
-
 void CharacterController::setCollisionlessAllowed(bool value) {
     if (value != _collisionlessAllowed) {
         _collisionlessAllowed = value;
diff --git a/libraries/physics/src/CharacterController.h b/libraries/physics/src/CharacterController.h
index d59374a94a..c46c9c8361 100755
--- a/libraries/physics/src/CharacterController.h
+++ b/libraries/physics/src/CharacterController.h
@@ -65,10 +65,10 @@ public:
     // overrides from btCharacterControllerInterface
     virtual void setWalkDirection(const btVector3 &walkDirection) override { assert(false); }
     virtual void setVelocityForTimeInterval(const btVector3 &velocity, btScalar timeInterval) override { assert(false); }
-    virtual void reset(btCollisionWorld* collisionWorld) override { }
-    virtual void warp(const btVector3& origin) override { }
-    virtual void debugDraw(btIDebugDraw* debugDrawer) override { }
-    virtual void setUpInterpolate(bool value) override { }
+    virtual void reset(btCollisionWorld* collisionWorld) override {}
+    virtual void warp(const btVector3& origin) override {}
+    virtual void debugDraw(btIDebugDraw* debugDrawer) override {}
+    virtual void setUpInterpolate(bool value) override {}
     virtual void updateAction(btCollisionWorld* collisionWorld, btScalar deltaTime) override;
     virtual void preStep(btCollisionWorld *collisionWorld) override;
     virtual void playerStep(btCollisionWorld *collisionWorld, btScalar dt) override;
@@ -90,7 +90,7 @@ public:
     void preSimulation();
     void postSimulation();
 
-    void setPositionAndOrientation( const glm::vec3& position, const glm::quat& orientation);
+    void setPositionAndOrientation(const glm::vec3& position, const glm::quat& orientation);
     void getPositionAndOrientation(glm::vec3& position, glm::quat& rotation) const;
 
     void setParentVelocity(const glm::vec3& parentVelocity);
@@ -129,7 +129,8 @@ public:
 
     bool getRigidBodyLocation(glm::vec3& avatarRigidBodyPosition, glm::quat& avatarRigidBodyRotation);
 
-    void setFlyingAllowed(bool value);
+    void setZoneFlyingAllowed(bool value) { _zoneFlyingAllowed = value; }
+    void setComfortFlyingAllowed(bool value) { _comfortFlyingAllowed = value; }
     void setCollisionlessAllowed(bool value);
 
     void setPendingFlagsUpdateCollisionMask(){ _pendingFlags |= PENDING_FLAG_UPDATE_COLLISION_MASK; }
@@ -212,7 +213,8 @@ protected:
     uint32_t _pendingFlags { 0 };
     uint32_t _previousFlags { 0 };
 
-    bool _flyingAllowed { true };
+    bool _zoneFlyingAllowed { true };
+    bool _comfortFlyingAllowed { true };
     bool _collisionlessAllowed { true };
     bool _collisionless { false };
 
diff --git a/libraries/physics/src/ObjectActionTractor.cpp b/libraries/physics/src/ObjectActionTractor.cpp
index a46aac3f29..c7681e217c 100644
--- a/libraries/physics/src/ObjectActionTractor.cpp
+++ b/libraries/physics/src/ObjectActionTractor.cpp
@@ -60,7 +60,7 @@ bool ObjectActionTractor::getTarget(float deltaTimeStep, glm::quat& rotation, gl
             }
             if (other && otherIsReady) {
                 bool success;
-                glm::vec3 otherWorldPosition = other->getWorldPosition(_otherJointIndex, success);
+                glm::vec3 otherWorldPosition = other->getJointWorldPosition(_otherJointIndex, success);
                 if (!success) {
                     linearTimeScale = FLT_MAX;
                     angularTimeScale = FLT_MAX;
diff --git a/libraries/pointers/src/PickManager.cpp b/libraries/pointers/src/PickManager.cpp
index d3326ea8b4..0cf5f90e3d 100644
--- a/libraries/pointers/src/PickManager.cpp
+++ b/libraries/pointers/src/PickManager.cpp
@@ -6,6 +6,8 @@
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
 //
 #include "PickManager.h"
+#include "PerfStat.h"
+#include "Profile.h"
 
 PickManager::PickManager() {
     setShouldPickHUDOperator([]() { return false; });
@@ -119,10 +121,26 @@ void PickManager::update() {
     bool shouldPickHUD = _shouldPickHUDOperator();
     // FIXME: give each type its own expiry
     // Each type will update at least one pick, regardless of the expiry
-    _updatedPickCounts[PickQuery::Stylus] = _stylusPickCacheOptimizer.update(cachedPicks[PickQuery::Stylus], _nextPickToUpdate[PickQuery::Stylus], expiry, false);
-    _updatedPickCounts[PickQuery::Ray] = _rayPickCacheOptimizer.update(cachedPicks[PickQuery::Ray], _nextPickToUpdate[PickQuery::Ray], expiry, shouldPickHUD);
-    _updatedPickCounts[PickQuery::Parabola] = _parabolaPickCacheOptimizer.update(cachedPicks[PickQuery::Parabola], _nextPickToUpdate[PickQuery::Parabola], expiry, shouldPickHUD);
-    _updatedPickCounts[PickQuery::Collision] = _collisionPickCacheOptimizer.update(cachedPicks[PickQuery::Collision], _nextPickToUpdate[PickQuery::Collision], expiry, false);
+    {
+        PROFILE_RANGE(picks, "StylusPicks");
+        PerformanceTimer perfTimer("StylusPicks");
+        _updatedPickCounts[PickQuery::Stylus] = _stylusPickCacheOptimizer.update(cachedPicks[PickQuery::Stylus], _nextPickToUpdate[PickQuery::Stylus], expiry, false);
+    }
+    {
+        PROFILE_RANGE(picks, "RayPicks");
+        PerformanceTimer perfTimer("RayPicks");
+        _updatedPickCounts[PickQuery::Ray] = _rayPickCacheOptimizer.update(cachedPicks[PickQuery::Ray], _nextPickToUpdate[PickQuery::Ray], expiry, shouldPickHUD);
+    }
+    {
+        PROFILE_RANGE(picks, "ParabolaPick");
+        PerformanceTimer perfTimer("ParabolaPick");
+        _updatedPickCounts[PickQuery::Parabola] = _parabolaPickCacheOptimizer.update(cachedPicks[PickQuery::Parabola], _nextPickToUpdate[PickQuery::Parabola], expiry, shouldPickHUD);
+    }
+    {
+        PROFILE_RANGE(picks, "CollisoinPicks");
+        PerformanceTimer perfTimer("CollisionPicks");
+        _updatedPickCounts[PickQuery::Collision] = _collisionPickCacheOptimizer.update(cachedPicks[PickQuery::Collision], _nextPickToUpdate[PickQuery::Collision], expiry, false);
+    }
 }
 
 bool PickManager::isLeftHand(unsigned int uid) {
diff --git a/libraries/render-utils/src/AmbientOcclusionEffect.cpp b/libraries/render-utils/src/AmbientOcclusionEffect.cpp
index b1ca24de1f..ee53347158 100644
--- a/libraries/render-utils/src/AmbientOcclusionEffect.cpp
+++ b/libraries/render-utils/src/AmbientOcclusionEffect.cpp
@@ -205,7 +205,7 @@ gpu::TexturePointer AmbientOcclusionFramebuffer::getNormalTexture() {
 }
 
 AmbientOcclusionEffectConfig::AmbientOcclusionEffectConfig() :
-    render::GPUJobConfig::Persistent(QStringList() << "Render" << "Engine" << "Ambient Occlusion", false),
+    render::GPUJobConfig::Persistent(QStringList() << "Render" << "Engine" << "Ambient Occlusion"),
     perspectiveScale{ 1.0f },
     edgeSharpness{ 1.0f },
     blurRadius{ 4 },
diff --git a/libraries/render-utils/src/CauterizedModel.cpp b/libraries/render-utils/src/CauterizedModel.cpp
index 81a81c5602..cfb78d6bbc 100644
--- a/libraries/render-utils/src/CauterizedModel.cpp
+++ b/libraries/render-utils/src/CauterizedModel.cpp
@@ -178,6 +178,7 @@ void CauterizedModel::updateClusterMatrices() {
             }
         }
     }
+    computeMeshPartLocalBounds();
 
     // post the blender if we're not currently waiting for one to finish
     auto modelBlender = DependencyManager::get();
diff --git a/libraries/render-utils/src/LightingModel.h b/libraries/render-utils/src/LightingModel.h
index 571eadb60b..f6bd6dcd46 100644
--- a/libraries/render-utils/src/LightingModel.h
+++ b/libraries/render-utils/src/LightingModel.h
@@ -118,7 +118,7 @@ protected:
         float enableSkinning{ 1.0f };
         float enableBlendshape{ 1.0f };
 
-        float enableAmbientOcclusion{ 0.0f };
+        float enableAmbientOcclusion{ 0.0f }; // false by default
         float enableShadow{ 1.0f };
         float spare1{ 1.0f };
         float spare2{ 1.0f };
@@ -196,15 +196,13 @@ public:
     bool enableSkinning{ true };
     bool enableBlendshape{ true };
 
-    bool enableAmbientOcclusion{ true };
+    bool enableAmbientOcclusion{ false }; // false by default
     bool enableShadow{ true };
 
 
     void setAmbientOcclusion(bool enable) { enableAmbientOcclusion = enable; emit dirty();}
     bool isAmbientOcclusionEnabled() const { return enableAmbientOcclusion; }
-    void setShadow(bool enable) { 
-        enableShadow = enable; emit dirty();
-     }
+    void setShadow(bool enable) { enableShadow = enable; emit dirty(); }
     bool isShadowEnabled() const { return enableShadow; }
 
 signals:
diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp
index 6409cdd231..b1104b8aad 100644
--- a/libraries/render-utils/src/MeshPartPayload.cpp
+++ b/libraries/render-utils/src/MeshPartPayload.cpp
@@ -83,7 +83,7 @@ void MeshPartPayload::updateKey(const render::ItemKey& key) {
     ItemKey::Builder builder(key);
     builder.withTypeShape();
 
-    if (_drawMaterials.needsUpdate() || _drawMaterials.areTexturesLoading()) {
+    if (_drawMaterials.shouldUpdate()) {
         RenderPipelines::updateMultiMaterial(_drawMaterials);
     }
 
@@ -329,7 +329,7 @@ void ModelMeshPartPayload::updateKey(const render::ItemKey& key) {
         builder.withDeformed();
     }
 
-    if (_drawMaterials.needsUpdate() || _drawMaterials.areTexturesLoading()) {
+    if (_drawMaterials.shouldUpdate()) {
         RenderPipelines::updateMultiMaterial(_drawMaterials);
     }
 
@@ -347,7 +347,7 @@ void ModelMeshPartPayload::setShapeKey(bool invalidateShapeKey, PrimitiveMode pr
         return;
     }
 
-    if (_drawMaterials.needsUpdate() || _drawMaterials.areTexturesLoading()) {
+    if (_drawMaterials.shouldUpdate()) {
         RenderPipelines::updateMultiMaterial(_drawMaterials);
     }
 
diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp
index b9b294d0e3..a8d3e504f1 100644
--- a/libraries/render-utils/src/Model.cpp
+++ b/libraries/render-utils/src/Model.cpp
@@ -1169,6 +1169,7 @@ void Model::setURL(const QUrl& url) {
         resource->setLoadPriority(this, _loadingPriority);
         _renderWatcher.setResource(resource);
     }
+    _rig.initFlow(false);
     onInvalidate();
 }
 
@@ -1345,14 +1346,19 @@ void Model::updateRig(float deltaTime, glm::mat4 parentTransform) {
 }
 
 void Model::computeMeshPartLocalBounds() {
-    for (auto& part : _modelMeshRenderItems) {
-        const Model::MeshState& state = _meshStates.at(part->_meshIndex);
-        if (_useDualQuaternionSkinning) {
-            part->computeAdjustedLocalBound(state.clusterDualQuaternions);
-        } else {
-            part->computeAdjustedLocalBound(state.clusterMatrices);
-        }
+    render::Transaction transaction;
+    auto meshStates = _meshStates;
+    for (auto renderItem : _modelMeshRenderItemIDs) {
+        transaction.updateItem(renderItem, [this, meshStates](ModelMeshPartPayload& data) {
+            const Model::MeshState& state = meshStates.at(data._meshIndex);
+            if (_useDualQuaternionSkinning) {
+                data.computeAdjustedLocalBound(state.clusterDualQuaternions);
+            } else {
+                data.computeAdjustedLocalBound(state.clusterMatrices);
+            }
+        });
     }
+    AbstractViewStateInterface::instance()->getMain3DScene()->enqueueTransaction(transaction);
 }
 
 // virtual
@@ -1385,6 +1391,7 @@ void Model::updateClusterMatrices() {
             }
         }
     }
+    computeMeshPartLocalBounds();
 
     // post the blender if we're not currently waiting for one to finish
     auto modelBlender = DependencyManager::get();
@@ -1649,9 +1656,9 @@ void packBlendshapeOffsetTo_Pos_F32_3xSN10_Nor_3xSN10_Tan_3xSN10(glm::uvec4& pac
 
     packed = glm::uvec4(
         glm::floatBitsToUint(len),
-        glm::packSnorm3x10_1x2(glm::vec4(normalizedPos, 0.0f)),
-        glm::packSnorm3x10_1x2(glm::vec4(unpacked.normalOffset, 0.0f)),
-        glm::packSnorm3x10_1x2(glm::vec4(unpacked.tangentOffset, 0.0f))
+        glm_packSnorm3x10_1x2(glm::vec4(normalizedPos, 0.0f)),
+        glm_packSnorm3x10_1x2(glm::vec4(unpacked.normalOffset, 0.0f)),
+        glm_packSnorm3x10_1x2(glm::vec4(unpacked.tangentOffset, 0.0f))
     );
 }
 
diff --git a/libraries/render-utils/src/RenderForwardTask.cpp b/libraries/render-utils/src/RenderForwardTask.cpp
index df82d4b56d..73692b41c2 100755
--- a/libraries/render-utils/src/RenderForwardTask.cpp
+++ b/libraries/render-utils/src/RenderForwardTask.cpp
@@ -96,13 +96,6 @@ void RenderForwardTask::build(JobModel& task, const render::Varying& input, rend
     // draw a stencil mask in hidden regions of the framebuffer.
     task.addJob("PrepareStencil", framebuffer);
 
-    // Layered
-    const auto nullJitter = Varying(glm::vec2(0.0f, 0.0f));
-    const auto inFrontOpaquesInputs = DrawLayered3D::Inputs(inFrontOpaque, lightingModel, nullJitter).asVarying();
-    const auto inFrontTransparentsInputs = DrawLayered3D::Inputs(inFrontTransparent, lightingModel, nullJitter).asVarying();
-    task.addJob("DrawInFrontOpaque", inFrontOpaquesInputs, true);
-    task.addJob("DrawInFrontTransparent", inFrontTransparentsInputs, false);
-
     // Draw opaques forward
     const auto opaqueInputs = DrawForward::Inputs(opaques, lightingModel).asVarying();
     task.addJob("DrawOpaques", opaqueInputs, shapePlumber);
@@ -115,6 +108,13 @@ void RenderForwardTask::build(JobModel& task, const render::Varying& input, rend
     const auto transparentInputs = DrawForward::Inputs(transparents, lightingModel).asVarying();
     task.addJob("DrawTransparents", transparentInputs, shapePlumber);
 
+     // Layered
+    const auto nullJitter = Varying(glm::vec2(0.0f, 0.0f));
+    const auto inFrontOpaquesInputs = DrawLayered3D::Inputs(inFrontOpaque, lightingModel, nullJitter).asVarying();
+    const auto inFrontTransparentsInputs = DrawLayered3D::Inputs(inFrontTransparent, lightingModel, nullJitter).asVarying();
+    task.addJob("DrawInFrontOpaque", inFrontOpaquesInputs, true);
+    task.addJob("DrawInFrontTransparent", inFrontTransparentsInputs, false);
+
     {  // Debug the bounds of the rendered items, still look at the zbuffer
 
         task.addJob("DrawMetaBounds", metas);
diff --git a/libraries/render-utils/src/RenderPipelines.cpp b/libraries/render-utils/src/RenderPipelines.cpp
index 6f6f2ab856..c4a7368f39 100644
--- a/libraries/render-utils/src/RenderPipelines.cpp
+++ b/libraries/render-utils/src/RenderPipelines.cpp
@@ -382,11 +382,6 @@ void RenderPipelines::bindMaterial(graphics::MaterialPointer& material, gpu::Bat
 void RenderPipelines::updateMultiMaterial(graphics::MultiMaterial& multiMaterial) {
     auto& schemaBuffer = multiMaterial.getSchemaBuffer();
 
-    if (multiMaterial.size() == 0) {
-        schemaBuffer.edit() = graphics::MultiMaterial::Schema();
-        return;
-    }
-
     auto& drawMaterialTextures = multiMaterial.getTextureTable();
     multiMaterial.setTexturesLoading(false);
 
@@ -732,14 +727,11 @@ void RenderPipelines::updateMultiMaterial(graphics::MultiMaterial& multiMaterial
     schema._key = (uint32_t)schemaKey._flags.to_ulong();
     schemaBuffer.edit() = schema;
     multiMaterial.setNeedsUpdate(false);
+    multiMaterial.setInitialized();
 }
 
 void RenderPipelines::bindMaterials(graphics::MultiMaterial& multiMaterial, gpu::Batch& batch, bool enableTextures) {
-    if (multiMaterial.size() == 0) {
-        return;
-    }
-
-    if (multiMaterial.needsUpdate() || multiMaterial.areTexturesLoading()) {
+    if (multiMaterial.shouldUpdate()) {
         updateMultiMaterial(multiMaterial);
     }
 
diff --git a/libraries/render-utils/src/parabola_forward.slv b/libraries/render-utils/src/parabola_forward.slv
new file mode 100644
index 0000000000..4eb1456666
--- /dev/null
+++ b/libraries/render-utils/src/parabola_forward.slv
@@ -0,0 +1,7 @@
+layout(location=0) in vec4 _color;
+
+layout(location=0) out vec4 _fragColor0;
+
+void main(void) {
+    _fragColor0 = _color;
+}
\ No newline at end of file
diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp
index 5d33a6a061..825017b1fe 100644
--- a/libraries/script-engine/src/ScriptEngine.cpp
+++ b/libraries/script-engine/src/ScriptEngine.cpp
@@ -976,7 +976,9 @@ void ScriptEngine::addEventHandler(const EntityItemID& entityID, const QString&
         using PointerHandler = std::function;
         auto makePointerHandler = [this](QString eventName) -> PointerHandler {
             return [this, eventName](const EntityItemID& entityItemID, const PointerEvent& event) {
-                forwardHandlerCall(entityItemID, eventName, { entityItemID.toScriptValue(this), event.toScriptValue(this) });
+                if (!EntityTree::areEntityClicksCaptured()) {
+                    forwardHandlerCall(entityItemID, eventName, { entityItemID.toScriptValue(this), event.toScriptValue(this) });
+                }
             };
         };
 
diff --git a/libraries/shaders/headers/310es/header.glsl b/libraries/shaders/headers/310es/header.glsl
index 9a0af85281..a21b2ec5d6 100644
--- a/libraries/shaders/headers/310es/header.glsl
+++ b/libraries/shaders/headers/310es/header.glsl
@@ -1,6 +1,3 @@
-#version 310 es
-#define GPU_GLES
-#define GPU_GLES_310
 #define BITFIELD highp int
 #define LAYOUT(X) layout(X)
 #define LAYOUT_STD140(X) layout(std140, X)
@@ -9,6 +6,9 @@
     #define gl_VertexID  gl_VertexIndex
 #endif
 #extension GL_EXT_texture_buffer : enable
+#if defined(HAVE_EXT_clip_cull_distance) && !defined(VULKAN)
+#extension GL_EXT_clip_cull_distance : enable
+#endif
 precision highp float;
 precision highp samplerBuffer;
 precision highp sampler2DShadow;
diff --git a/libraries/shaders/headers/310es/version.glsl b/libraries/shaders/headers/310es/version.glsl
new file mode 100644
index 0000000000..52e5edea8b
--- /dev/null
+++ b/libraries/shaders/headers/310es/version.glsl
@@ -0,0 +1,3 @@
+#version 310 es
+#define GPU_GLES
+#define GPU_GLES_310
diff --git a/libraries/shaders/headers/410/header.glsl b/libraries/shaders/headers/410/header.glsl
index 901ae6f9db..20bd6b2505 100644
--- a/libraries/shaders/headers/410/header.glsl
+++ b/libraries/shaders/headers/410/header.glsl
@@ -1,5 +1,3 @@
-#version 410 core
-#define GPU_GL410
 #define BITFIELD int
 #if defined(VULKAN)
   #extension GL_ARB_shading_language_420pack : require
diff --git a/libraries/shaders/headers/410/version.glsl b/libraries/shaders/headers/410/version.glsl
new file mode 100644
index 0000000000..01ab7e5398
--- /dev/null
+++ b/libraries/shaders/headers/410/version.glsl
@@ -0,0 +1,2 @@
+#version 410 core
+#define GPU_GL410
diff --git a/libraries/shaders/headers/450/header.glsl b/libraries/shaders/headers/450/header.glsl
index 6ce61b4378..ef0ec09414 100644
--- a/libraries/shaders/headers/450/header.glsl
+++ b/libraries/shaders/headers/450/header.glsl
@@ -1,5 +1,3 @@
-#version 450 core
-#define GPU_GL450
 #define GPU_SSBO_TRANSFORM_OBJECT
 #define BITFIELD int
 #define LAYOUT(X) layout(X)
diff --git a/libraries/shaders/headers/450/version.glsl b/libraries/shaders/headers/450/version.glsl
new file mode 100644
index 0000000000..874d578bd9
--- /dev/null
+++ b/libraries/shaders/headers/450/version.glsl
@@ -0,0 +1,2 @@
+#version 450 core
+#define GPU_GL450
diff --git a/libraries/shaders/src/shaders/Shaders.cpp b/libraries/shaders/src/shaders/Shaders.cpp
index c7e39b2940..a074c2f0c9 100644
--- a/libraries/shaders/src/shaders/Shaders.cpp
+++ b/libraries/shaders/src/shaders/Shaders.cpp
@@ -224,7 +224,7 @@ String Source::getSource(Dialect dialect, Variant variant) const {
         }
     }
 
-#ifdef Q_OS_ANDROID
+#if defined(Q_OS_ANDROID) || defined(USE_GLES)
     // SPIRV cross injects "#extension GL_OES_texture_buffer : require" into the GLSL shaders,
     // which breaks android rendering
     return variantSource.scribe;
diff --git a/libraries/shared/src/AvatarConstants.h b/libraries/shared/src/AvatarConstants.h
index 103782bd3f..d55a63b960 100644
--- a/libraries/shared/src/AvatarConstants.h
+++ b/libraries/shared/src/AvatarConstants.h
@@ -20,6 +20,7 @@ const float DEFAULT_AVATAR_EYE_TO_TOP_OF_HEAD = 0.11f; // meters
 const float DEFAULT_AVATAR_NECK_TO_TOP_OF_HEAD = 0.185f; // meters
 const float DEFAULT_AVATAR_NECK_HEIGHT = DEFAULT_AVATAR_HEIGHT - DEFAULT_AVATAR_NECK_TO_TOP_OF_HEAD;
 const float DEFAULT_AVATAR_EYE_HEIGHT = DEFAULT_AVATAR_HEIGHT - DEFAULT_AVATAR_EYE_TO_TOP_OF_HEAD;
+const float DEFAULT_SPINE2_SPLINE_PROPORTION = 0.71f;
 const float DEFAULT_AVATAR_SUPPORT_BASE_LEFT  = -0.25f;
 const float DEFAULT_AVATAR_SUPPORT_BASE_RIGHT =  0.25f;
 const float DEFAULT_AVATAR_SUPPORT_BASE_FRONT = -0.20f;
diff --git a/libraries/shared/src/CubicHermiteSpline.h b/libraries/shared/src/CubicHermiteSpline.h
index cdbc64308d..c83000996b 100644
--- a/libraries/shared/src/CubicHermiteSpline.h
+++ b/libraries/shared/src/CubicHermiteSpline.h
@@ -66,19 +66,19 @@ public:
         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;
-        glm::vec3 prevValue = this->operator()(alpha);
-        for (int i = 1; i < NUM_SUBDIVISIONS + 1; i++) {
-            glm::vec3 nextValue = this->operator()(alpha + DELTA);
-            accum += glm::distance(prevValue, nextValue);
-            alpha += DELTA;
-            _values[i] = accum;
-            prevValue = nextValue;
-        }
+
+        initValues();
+    }
+
+    CubicHermiteSplineFunctorWithArcLength(const glm::quat& tipRot, const glm::vec3& tipTrans, const glm::quat& baseRot, const glm::vec3& baseTrans, float baseGain = 1.0f, float tipGain = 1.0f) : CubicHermiteSplineFunctor() {
+
+        float linearDistance = glm::length(baseTrans - tipTrans);
+        _p0 = baseTrans;
+        _m0 = baseGain * linearDistance * (baseRot * Vectors::UNIT_Y);
+        _p1 = tipTrans;
+        _m1 = tipGain * linearDistance * (tipRot * Vectors::UNIT_Y);
+
+        initValues();
     }
 
     CubicHermiteSplineFunctorWithArcLength(const CubicHermiteSplineFunctorWithArcLength& orig) : CubicHermiteSplineFunctor(orig) {
@@ -110,6 +110,21 @@ public:
     }
 protected:
     float _values[NUM_SUBDIVISIONS + 1];
+
+    void initValues() {
+        // 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;
+        }
+
+    }
 };
 
 #endif // hifi_CubicHermiteSpline_h
diff --git a/libraries/shared/src/GLMHelpers.h b/libraries/shared/src/GLMHelpers.h
index e7aaace1ae..6deae695cd 100644
--- a/libraries/shared/src/GLMHelpers.h
+++ b/libraries/shared/src/GLMHelpers.h
@@ -315,6 +315,42 @@ inline void glm_mat4u_mul(const glm::mat4& m1, const glm::mat4& m2, glm::mat4& r
 #endif
 }
 
+//
+// Fast replacement of glm::packSnorm3x10_1x2()
+// The SSE2 version quantizes using round to nearest even.
+// The glm version quantizes using round away from zero.
+//
+inline uint32_t glm_packSnorm3x10_1x2(vec4 const& v) {
+
+    union i10i10i10i2 {
+        struct {
+            int x : 10;
+            int y : 10;
+            int z : 10;
+            int w : 2;
+        } data;
+        uint32_t pack;
+    } Result;
+
+#if GLM_ARCH & GLM_ARCH_SSE2_BIT
+    __m128 vclamp = _mm_min_ps(_mm_max_ps(_mm_loadu_ps((float*)&v[0]), _mm_set1_ps(-1.0f)), _mm_set1_ps(1.0f));
+    __m128i vpack = _mm_cvtps_epi32(_mm_mul_ps(vclamp, _mm_setr_ps(511.f, 511.f, 511.f, 1.f)));
+
+    Result.data.x = _mm_cvtsi128_si32(vpack);
+    Result.data.y = _mm_cvtsi128_si32(_mm_shuffle_epi32(vpack, _MM_SHUFFLE(1,1,1,1)));
+    Result.data.z = _mm_cvtsi128_si32(_mm_shuffle_epi32(vpack, _MM_SHUFFLE(2,2,2,2)));
+    Result.data.w = _mm_cvtsi128_si32(_mm_shuffle_epi32(vpack, _MM_SHUFFLE(3,3,3,3)));
+#else
+    ivec4 const Pack(round(clamp(v, -1.0f, 1.0f) * vec4(511.f, 511.f, 511.f, 1.f)));
+
+    Result.data.x = Pack.x;
+    Result.data.y = Pack.y;
+    Result.data.z = Pack.z;
+    Result.data.w = Pack.w;
+#endif
+    return Result.pack;
+}
+
 // convert float to int, using round-to-nearest-even (undefined on overflow)
 inline int fastLrintf(float x) {
 #if GLM_ARCH & GLM_ARCH_SSE2_BIT
diff --git a/libraries/shared/src/NestableTransformNode.h b/libraries/shared/src/NestableTransformNode.h
index be017a696d..a584bcd308 100644
--- a/libraries/shared/src/NestableTransformNode.h
+++ b/libraries/shared/src/NestableTransformNode.h
@@ -32,7 +32,7 @@ public:
         }
 
         bool success;
-        Transform jointWorldTransform = nestable->getTransform(_jointIndex, success);
+        Transform jointWorldTransform = nestable->getJointTransform(_jointIndex, success);
 
         if (!success) {
             return Transform();
diff --git a/libraries/shared/src/Profile.cpp b/libraries/shared/src/Profile.cpp
index 778b39aca5..272538e26d 100644
--- a/libraries/shared/src/Profile.cpp
+++ b/libraries/shared/src/Profile.cpp
@@ -12,6 +12,7 @@ Q_LOGGING_CATEGORY(trace_app, "trace.app")
 Q_LOGGING_CATEGORY(trace_app_detail, "trace.app.detail")
 Q_LOGGING_CATEGORY(trace_metadata, "trace.metadata")
 Q_LOGGING_CATEGORY(trace_network, "trace.network")
+Q_LOGGING_CATEGORY(trace_picks, "trace.picks")
 Q_LOGGING_CATEGORY(trace_parse, "trace.parse")
 Q_LOGGING_CATEGORY(trace_render, "trace.render")
 Q_LOGGING_CATEGORY(trace_render_detail, "trace.render.detail")
diff --git a/libraries/shared/src/Profile.h b/libraries/shared/src/Profile.h
index 01d86f8f2e..dc2ed6e754 100644
--- a/libraries/shared/src/Profile.h
+++ b/libraries/shared/src/Profile.h
@@ -18,6 +18,7 @@ Q_DECLARE_LOGGING_CATEGORY(trace_app)
 Q_DECLARE_LOGGING_CATEGORY(trace_app_detail)
 Q_DECLARE_LOGGING_CATEGORY(trace_metadata)
 Q_DECLARE_LOGGING_CATEGORY(trace_network)
+Q_DECLARE_LOGGING_CATEGORY(trace_picks)
 Q_DECLARE_LOGGING_CATEGORY(trace_render)
 Q_DECLARE_LOGGING_CATEGORY(trace_render_detail)
 Q_DECLARE_LOGGING_CATEGORY(trace_render_gpu)
diff --git a/libraries/shared/src/RegisteredMetaTypes.cpp b/libraries/shared/src/RegisteredMetaTypes.cpp
index 79085e05ba..597e537d8d 100644
--- a/libraries/shared/src/RegisteredMetaTypes.cpp
+++ b/libraries/shared/src/RegisteredMetaTypes.cpp
@@ -1272,7 +1272,14 @@ QVariantMap parseTexturesToMap(QString newTextures, const QVariantMap& defaultTe
     QVariantMap newTexturesMap = newTexturesJson.toVariant().toMap();
     QVariantMap toReturn = defaultTextures;
     for (auto& texture : newTexturesMap.keys()) {
-        toReturn[texture] = newTexturesMap[texture];
+        auto newURL = newTexturesMap[texture];
+        if (newURL.canConvert()) {
+            toReturn[texture] = newURL.toUrl();
+        } else if (newURL.canConvert()) {
+            toReturn[texture] = QUrl(newURL.toString());
+        } else {
+            toReturn[texture] = newURL;
+        }
     }
 
     return toReturn;
diff --git a/libraries/shared/src/SpatiallyNestable.cpp b/libraries/shared/src/SpatiallyNestable.cpp
index d3ed79faf4..4d97f43e6a 100644
--- a/libraries/shared/src/SpatiallyNestable.cpp
+++ b/libraries/shared/src/SpatiallyNestable.cpp
@@ -67,6 +67,7 @@ const QUuid SpatiallyNestable::getParentID() const {
 }
 
 void SpatiallyNestable::setParentID(const QUuid& parentID) {
+    bumpAncestorChainRenderableVersion();
     _idLock.withWriteLock([&] {
         if (_parentID != parentID) {
             _parentID = parentID;
@@ -78,6 +79,7 @@ void SpatiallyNestable::setParentID(const QUuid& parentID) {
         bool success = false;
         auto parent = getParentPointer(success);
         if (success && parent) {
+            bumpAncestorChainRenderableVersion();
             parent->updateQueryAACube();
         }
     }
@@ -90,7 +92,7 @@ Transform SpatiallyNestable::getParentTransform(bool& success, int depth) const
         return result;
     }
     if (parent) {
-        result = parent->getTransform(_parentJointIndex, success, depth + 1);
+        result = parent->getJointTransform(_parentJointIndex, success, depth + 1);
         if (getScalesWithParent()) {
             result.setScale(parent->scaleForChildren());
         }
@@ -201,7 +203,7 @@ glm::vec3 SpatiallyNestable::worldToLocal(const glm::vec3& position,
     }
 
     if (parent) {
-        parentTransform = parent->getTransform(parentJointIndex, success);
+        parentTransform = parent->getJointTransform(parentJointIndex, success);
         if (!success) {
             return glm::vec3(0.0f);
         }
@@ -238,7 +240,7 @@ glm::quat SpatiallyNestable::worldToLocal(const glm::quat& orientation,
     }
 
     if (parent) {
-        parentTransform = parent->getTransform(parentJointIndex, success);
+        parentTransform = parent->getJointTransform(parentJointIndex, success);
         if (!success) {
             return glm::quat();
         }
@@ -348,7 +350,7 @@ glm::vec3 SpatiallyNestable::localToWorld(const glm::vec3& position,
     }
 
     if (parent) {
-        parentTransform = parent->getTransform(parentJointIndex, success);
+        parentTransform = parent->getJointTransform(parentJointIndex, success);
         if (!success) {
             return glm::vec3(0.0f);
         }
@@ -388,7 +390,7 @@ glm::quat SpatiallyNestable::localToWorld(const glm::quat& orientation,
     }
 
     if (parent) {
-        parentTransform = parent->getTransform(parentJointIndex, success);
+        parentTransform = parent->getJointTransform(parentJointIndex, success);
         if (!success) {
             return glm::quat();
         }
@@ -523,8 +525,8 @@ glm::vec3 SpatiallyNestable::getWorldPosition() const {
     return result;
 }
 
-glm::vec3 SpatiallyNestable::getWorldPosition(int jointIndex, bool& success) const {
-    return getTransform(jointIndex, success).getTranslation();
+glm::vec3 SpatiallyNestable::getJointWorldPosition(int jointIndex, bool& success) const {
+    return getJointTransform(jointIndex, success).getTranslation();
 }
 
 void SpatiallyNestable::setWorldPosition(const glm::vec3& position, bool& success, bool tellPhysics) {
@@ -577,7 +579,7 @@ glm::quat SpatiallyNestable::getWorldOrientation() const {
 }
 
 glm::quat SpatiallyNestable::getWorldOrientation(int jointIndex, bool& success) const {
-    return getTransform(jointIndex, success).getRotation();
+    return getJointTransform(jointIndex, success).getRotation();
 }
 
 void SpatiallyNestable::setWorldOrientation(const glm::quat& orientation, bool& success, bool tellPhysics) {
@@ -763,7 +765,7 @@ void SpatiallyNestable::breakParentingLoop() const {
     }
 }
 
-const Transform SpatiallyNestable::getTransform(int jointIndex, bool& success, int depth) const {
+const Transform SpatiallyNestable::getJointTransform(int jointIndex, bool& success, int depth) const {
     // this returns the world-space transform for this object.  It finds its parent's transform (which may
     // cause this object's parent to query its parent, etc) and multiplies this object's local transform onto it.
     Transform jointInWorldFrame;
@@ -830,8 +832,8 @@ glm::vec3 SpatiallyNestable::getSNScale(bool& success) const {
     return getTransform(success).getScale();
 }
 
-glm::vec3 SpatiallyNestable::getSNScale(int jointIndex, bool& success) const {
-    return getTransform(jointIndex, success).getScale();
+glm::vec3 SpatiallyNestable::getJointSNScale(int jointIndex, bool& success) const {
+    return getJointTransform(jointIndex, success).getScale();
 }
 
 void SpatiallyNestable::setSNScale(const glm::vec3& scale) {
@@ -859,7 +861,7 @@ void SpatiallyNestable::setSNScale(const glm::vec3& scale, bool& success) {
         }
     });
     if (success && changed) {
-        locationChanged();
+        dimensionsChanged();
     }
 }
 
@@ -1098,10 +1100,12 @@ void SpatiallyNestable::forEachDescendantTest(const ChildLambdaTest& actor) cons
     }
 }
 
-void SpatiallyNestable::locationChanged(bool tellPhysics) {
-    forEachChild([&](SpatiallyNestablePointer object) {
-        object->locationChanged(tellPhysics);
-    });
+void SpatiallyNestable::locationChanged(bool tellPhysics, bool tellChildren) {
+    if (tellChildren) {
+        forEachChild([&](SpatiallyNestablePointer object) {
+            object->locationChanged(tellPhysics, tellChildren);
+        });
+    }
 }
 
 AACube SpatiallyNestable::getMaximumAACube(bool& success) const {
@@ -1337,8 +1341,6 @@ QString SpatiallyNestable::nestableTypeToString(NestableType nestableType) {
             return "entity";
         case NestableType::Avatar:
             return "avatar";
-        case NestableType::Overlay:
-            return "overlay";
         default:
             return "unknown";
     }
@@ -1419,3 +1421,17 @@ QUuid SpatiallyNestable::getEditSenderID() {
     });
     return editSenderID;
 }
+
+void SpatiallyNestable::bumpAncestorChainRenderableVersion(int depth) const {
+    if (depth > MAX_PARENTING_CHAIN_SIZE) {
+        // can't break the parent chain here, because it will call setParentID, which calls this
+        return;
+    }
+
+    _ancestorChainRenderableVersion++;
+    bool success = false;
+    auto parent = getParentPointer(success);
+    if (success && parent) {
+        parent->bumpAncestorChainRenderableVersion(depth + 1);
+    }
+}
diff --git a/libraries/shared/src/SpatiallyNestable.h b/libraries/shared/src/SpatiallyNestable.h
index e7a449f73f..f52dc4bf8b 100644
--- a/libraries/shared/src/SpatiallyNestable.h
+++ b/libraries/shared/src/SpatiallyNestable.h
@@ -30,8 +30,7 @@ static const uint16_t INVALID_JOINT_INDEX = -1;
 
 enum class NestableType {
     Entity,
-    Avatar,
-    Overlay
+    Avatar
 };
 
 class SpatiallyNestable : public std::enable_shared_from_this {
@@ -130,9 +129,9 @@ public:
     virtual void setSNScale(const glm::vec3& scale, bool& success);
 
     // get world-frame values for a specific joint
-    virtual const Transform getTransform(int jointIndex, bool& success, int depth = 0) const;
-    virtual glm::vec3 getWorldPosition(int jointIndex, bool& success) const;
-    virtual glm::vec3 getSNScale(int jointIndex, bool& success) const;
+    virtual const Transform getJointTransform(int jointIndex, bool& success, int depth = 0) const;
+    virtual glm::vec3 getJointWorldPosition(int jointIndex, bool& success) const;
+    virtual glm::vec3 getJointSNScale(int jointIndex, bool& success) const;
 
     // object's parent's frame
     virtual Transform getLocalTransform() const;
@@ -212,7 +211,7 @@ public:
 
     void dump(const QString& prefix = "") const;
 
-    virtual void locationChanged(bool tellPhysics = true); // called when a this object's location has changed
+    virtual void locationChanged(bool tellPhysics = true, bool tellChildren = true); // called when a this object's location has changed
     virtual void dimensionsChanged() { _queryAACubeSet = false; } // called when a this object's dimensions have changed
     virtual void parentDeleted() { } // called on children of a deleted parent
 
@@ -222,6 +221,8 @@ public:
     bool hasGrabs();
     virtual QUuid getEditSenderID();
 
+    void bumpAncestorChainRenderableVersion(int depth = 0) const;
+
 protected:
     QUuid _id;
     mutable SpatiallyNestableWeakPointer _parent;
@@ -244,6 +245,8 @@ protected:
     mutable ReadWriteLockable _grabsLock;
     QSet _grabs; // upon this thing
 
+    mutable std::atomic _ancestorChainRenderableVersion { 0 };
+
 private:
     SpatiallyNestable() = delete;
     const NestableType _nestableType; // EntityItem or an AvatarData
diff --git a/libraries/shared/src/VariantMapToScriptValue.cpp b/libraries/shared/src/VariantMapToScriptValue.cpp
index 1a747a4e5b..437f60a2ed 100644
--- a/libraries/shared/src/VariantMapToScriptValue.cpp
+++ b/libraries/shared/src/VariantMapToScriptValue.cpp
@@ -26,10 +26,10 @@ QScriptValue variantToScriptValue(QVariant& qValue, QScriptEngine& scriptEngine)
         case QVariant::Double:
             return qValue.toDouble();
             break;
-        case QVariant::String: {
+        case QVariant::String:
+        case QVariant::Url:
             return scriptEngine.newVariant(qValue);
             break;
-        }
         case QVariant::Map: {
             QVariantMap childMap = qValue.toMap();
             return variantMapToScriptValue(childMap, scriptEngine);
diff --git a/libraries/ui/src/QmlFragmentClass.cpp b/libraries/ui/src/QmlFragmentClass.cpp
index 13e3527ded..fbd045fdb1 100644
--- a/libraries/ui/src/QmlFragmentClass.cpp
+++ b/libraries/ui/src/QmlFragmentClass.cpp
@@ -24,7 +24,7 @@ QmlFragmentClass::QmlFragmentClass(bool restricted, QString id) : QmlWindowClass
 
 // Method called by Qt scripts to create a new bottom menu bar in Android
 QScriptValue QmlFragmentClass::internal_constructor(QScriptContext* context, QScriptEngine* engine, bool restricted) {
-
+#ifndef DISABLE_QML
     std::lock_guard guard(_mutex);
     auto qml = context->argument(0).toVariant().toMap().value("qml");
     if (qml.isValid()) {
@@ -53,6 +53,9 @@ QScriptValue QmlFragmentClass::internal_constructor(QScriptContext* context, QSc
     QScriptValue scriptObject = engine->newQObject(retVal);
     _fragments[qml.toString()] = scriptObject;
     return scriptObject;
+#else
+    return QScriptValue();
+#endif
 }
 
 void QmlFragmentClass::close() {
@@ -61,6 +64,7 @@ void QmlFragmentClass::close() {
 }
 
 QObject* QmlFragmentClass::addButton(const QVariant& properties) {
+#ifndef DISABLE_QML
     QVariant resultVar;
     Qt::ConnectionType connectionType = Qt::AutoConnection;
     
@@ -79,8 +83,10 @@ QObject* QmlFragmentClass::addButton(const QVariant& properties) {
         qWarning() << "QmlFragmentClass addButton result not a QObject";
         return NULL;
     }
-    
     return qmlButton;
+#else
+    return nullptr;
+#endif
 }
 
 void QmlFragmentClass::removeButton(QObject* button) {
diff --git a/libraries/ui/src/QmlWindowClass.cpp b/libraries/ui/src/QmlWindowClass.cpp
index 0182e3adc3..1140dbb079 100644
--- a/libraries/ui/src/QmlWindowClass.cpp
+++ b/libraries/ui/src/QmlWindowClass.cpp
@@ -98,6 +98,7 @@ QmlWindowClass::QmlWindowClass(bool restricted) : _restricted(restricted) {
  * @property {boolean} visible
  */
 void QmlWindowClass::initQml(QVariantMap properties) {
+#ifndef DISABLE_QML
     auto offscreenUi = DependencyManager::get();
     _source = properties[SOURCE_PROPERTY].toString();
 
@@ -150,6 +151,7 @@ void QmlWindowClass::initQml(QVariantMap properties) {
 
     Q_ASSERT(_qmlWindow);
     Q_ASSERT(dynamic_cast(_qmlWindow.data()));
+#endif
 }
 
 void QmlWindowClass::qmlToScript(const QVariant& message) {
diff --git a/libraries/ui/src/VirtualPadManager.cpp b/libraries/ui/src/VirtualPadManager.cpp
index ef2b8670cc..bf325a0bd6 100644
--- a/libraries/ui/src/VirtualPadManager.cpp
+++ b/libraries/ui/src/VirtualPadManager.cpp
@@ -38,10 +38,10 @@ namespace VirtualPad {
     const float Manager::BASE_DIAMETER_PIXELS = 512.0f;
     const float Manager::BASE_MARGIN_PIXELS = 59.0f;
     const float Manager::STICK_RADIUS_PIXELS = 105.0f;
-    const float Manager::JUMP_BTN_TRIMMED_RADIUS_PIXELS = 67.0f;
-    const float Manager::JUMP_BTN_FULL_PIXELS = 164.0f;
-    const float Manager::JUMP_BTN_BOTTOM_MARGIN_PIXELS = 80.0f;
-    const float Manager::JUMP_BTN_RIGHT_MARGIN_PIXELS = 13.0f;
+    const float Manager::BTN_TRIMMED_RADIUS_PIXELS = 67.0f;
+    const float Manager::BTN_FULL_PIXELS = 164.0f;
+    const float Manager::BTN_BOTTOM_MARGIN_PIXELS = 80.0f;
+    const float Manager::BTN_RIGHT_MARGIN_PIXELS = 13.0f;
 
     Manager::Manager() {
 
@@ -76,14 +76,6 @@ namespace VirtualPad {
         _extraBottomMargin = margin;
     }
 
-    glm::vec2 Manager::getJumpButtonPosition() {
-        return _jumpButtonPosition;
-    }
-
-    void Manager::setJumpButtonPosition(glm::vec2 point) {
-        _jumpButtonPosition = point;
-    }
-
     void Manager::requestHapticFeedback(int duration) {
         emit hapticFeedbackRequested(duration);
     }
@@ -92,6 +84,17 @@ namespace VirtualPad {
         return &_leftVPadInstance;
     }
 
+    glm::vec2 Manager::getButtonPosition(Manager::Button button) {
+        if (_buttonsPositions.count(button)) {
+            return _buttonsPositions.at(button);
+        }
+        return glm::vec2();
+    }
+
+    void Manager::setButtonPosition(Manager::Button button, glm::vec2 point) {
+        _buttonsPositions[button] = point;
+    }
+
     bool Instance::isShown() {
         return _shown;
     }
diff --git a/libraries/ui/src/VirtualPadManager.h b/libraries/ui/src/VirtualPadManager.h
index 3c3aa9ec9f..a1f2e17597 100644
--- a/libraries/ui/src/VirtualPadManager.h
+++ b/libraries/ui/src/VirtualPadManager.h
@@ -44,28 +44,35 @@ namespace VirtualPad {
         void hide(bool hide);
         int extraBottomMargin();
         void setExtraBottomMargin(int margin);
-        glm::vec2 getJumpButtonPosition();
-        void setJumpButtonPosition(glm::vec2 point);
+
+        enum Button {
+            JUMP,
+            HANDSHAKE
+        };
+
+        glm::vec2 getButtonPosition(Button button);
+        void setButtonPosition(Button button, glm::vec2 point);
+
         void requestHapticFeedback(int duration);
 
         static const float DPI;
         static const float BASE_DIAMETER_PIXELS;
         static const float BASE_MARGIN_PIXELS;
         static const float STICK_RADIUS_PIXELS;
-        static const float JUMP_BTN_TRIMMED_RADIUS_PIXELS;
-        static const float JUMP_BTN_FULL_PIXELS;
-        static const float JUMP_BTN_BOTTOM_MARGIN_PIXELS;
-        static const float JUMP_BTN_RIGHT_MARGIN_PIXELS;
+        static const float BTN_TRIMMED_RADIUS_PIXELS;
+        static const float BTN_FULL_PIXELS;
+        static const float BTN_BOTTOM_MARGIN_PIXELS;
+        static const float BTN_RIGHT_MARGIN_PIXELS;
 
     signals:
         void hapticFeedbackRequested(int duration);
 
     private:
         Instance _leftVPadInstance;
-        bool _enabled {true};
+        bool _enabled { true };
         bool _hidden;
-        glm::vec2 _jumpButtonPosition;
-        int _extraBottomMargin {0};
+        int _extraBottomMargin { 0 };
+        std::map _buttonsPositions;
     };
 }
 
diff --git a/libraries/ui/src/ui/TabletScriptingInterface.cpp b/libraries/ui/src/ui/TabletScriptingInterface.cpp
index a78b9a17fc..7a1c37af33 100644
--- a/libraries/ui/src/ui/TabletScriptingInterface.cpp
+++ b/libraries/ui/src/ui/TabletScriptingInterface.cpp
@@ -458,6 +458,11 @@ void TabletProxy::emitWebEvent(const QVariant& msg) {
 }
 
 void TabletProxy::onTabletShown() {
+    if (QThread::currentThread() != thread()) {
+        QMetaObject::invokeMethod(this, "onTabletShown");
+        return;
+    }
+
     if (_tabletShown) {
         Setting::Handle notificationSounds{ QStringLiteral("play_notification_sounds"), true};
         Setting::Handle notificationSoundTablet{ QStringLiteral("play_notification_sounds_tablet"), true};
@@ -485,7 +490,11 @@ bool TabletProxy::isPathLoaded(const QVariant& path) {
 }
 
 void TabletProxy::setQmlTabletRoot(OffscreenQmlSurface* qmlOffscreenSurface) {
-    Q_ASSERT(QThread::currentThread() == qApp->thread());
+    if (QThread::currentThread() != thread()) {
+        QMetaObject::invokeMethod(this, "setQmlTabletRoot", Q_ARG(OffscreenQmlSurface*, qmlOffscreenSurface));
+        return;
+    }
+
     _qmlOffscreenSurface = qmlOffscreenSurface;
     _qmlTabletRoot = qmlOffscreenSurface ? qmlOffscreenSurface->getRootItem() : nullptr;
     if (_qmlTabletRoot && _qmlOffscreenSurface) {
@@ -654,6 +663,11 @@ void TabletProxy::loadQMLSource(const QVariant& path, bool resizable) {
 }
 
 void TabletProxy::stopQMLSource() {
+    if (QThread::currentThread() != thread()) {
+        QMetaObject::invokeMethod(this, "stopQMLSource");
+        return;
+    }
+
     // For desktop toolbar mode dialogs.
     if (!_toolbarMode || !_desktopWindow) {
         qCDebug(uiLogging) << "tablet cannot clear QML because not desktop toolbar mode";
@@ -879,6 +893,12 @@ void TabletProxy::sendToQml(const QVariant& msg) {
 
 
 OffscreenQmlSurface* TabletProxy::getTabletSurface() {
+    if (QThread::currentThread() != thread()) {
+        OffscreenQmlSurface* result = nullptr;
+        BLOCKING_INVOKE_METHOD(this, "getTabletSurface", Q_RETURN_ARG(OffscreenQmlSurface*, result));
+        return result;
+    }
+
     return _qmlOffscreenSurface;
 }
 
@@ -888,6 +908,11 @@ void TabletProxy::desktopWindowClosed() {
 }
 
 void TabletProxy::unfocus() {
+    if (QThread::currentThread() != thread()) {
+        QMetaObject::invokeMethod(this, "unfocus");
+        return;
+    }
+
     if (_qmlOffscreenSurface) {
         _qmlOffscreenSurface->lowerKeyboard();
     }
diff --git a/prebuild.py b/prebuild.py
index 060e1fd3b0..5325ca34bc 100644
--- a/prebuild.py
+++ b/prebuild.py
@@ -58,6 +58,9 @@ logging.setLoggerClass(TrackableLogger)
 logger = logging.getLogger('prebuild')
 
 def headSha():
+    if shutil.which('git') is None:
+        logger.warn("Unable to find git executable, can't caclulate commit ID")
+        return '0xDEADBEEF'
     repo_dir = os.path.dirname(os.path.abspath(__file__))
     git = subprocess.Popen(
         'git rev-parse --short HEAD',
@@ -67,7 +70,7 @@ def headSha():
     stdout, _ = git.communicate()
     sha = stdout.split('\n')[0]
     if not sha:
-        raise RuntimeError("couldn't find git sha")
+        raise RuntimeError("couldn't find git sha for repository {}".format(repo_dir))
     return sha
 
 @contextmanager
diff --git a/scripts/+android_interface/defaultScripts.js b/scripts/+android_interface/defaultScripts.js
index e6971f5a6b..8b3082d81a 100644
--- a/scripts/+android_interface/defaultScripts.js
+++ b/scripts/+android_interface/defaultScripts.js
@@ -16,7 +16,8 @@ var DEFAULT_SCRIPTS_COMBINED = [
     "system/+android_interface/touchscreenvirtualpad.js",
     "system/+android_interface/actionbar.js",
     "system/+android_interface/audio.js" ,
-    "system/+android_interface/modes.js"/*,
+    "system/+android_interface/modes.js",
+    "system/makeUserConnection.js"/*,
     "system/away.js",
     "system/controllers/controllerDisplayManager.js",
     "system/controllers/handControllerGrabAndroid.js",
diff --git a/scripts/+android_questInterface/defaultScripts.js b/scripts/+android_questInterface/defaultScripts.js
index d22716302c..c294537419 100644
--- a/scripts/+android_questInterface/defaultScripts.js
+++ b/scripts/+android_questInterface/defaultScripts.js
@@ -14,8 +14,8 @@
 var DEFAULT_SCRIPTS_COMBINED = [
     "system/request-service.js",
     "system/progress.js",
-    //"system/away.js",
-    "system/hmd.js",
+    "system/away.js",
+    //"system/hmd.js",
     "system/menu.js",
     "system/bubble.js",
     "system/pal.js", // "system/mod.js", // older UX, if you prefer
@@ -25,6 +25,7 @@ var DEFAULT_SCRIPTS_COMBINED = [
     "system/notifications.js",
     "system/commerce/wallet.js",
     "system/dialTone.js",
+    "system/marketplaces/marketplaces.js",
     "system/quickGoto.js",
     "system/firstPersonHMD.js",
     "system/tablet-ui/tabletUI.js",
diff --git a/scripts/developer/debugging/debugAvatarMixer.js b/scripts/developer/debugging/debugAvatarMixer.js
index 90f2de13a9..fad4283f7c 100644
--- a/scripts/developer/debugging/debugAvatarMixer.js
+++ b/scripts/developer/debugging/debugAvatarMixer.js
@@ -19,6 +19,11 @@ Script.include("/~/system/libraries/controllers.js");
 
 var isShowingOverlays = true;
 var debugOverlays = {};
+var textSizeOverlay = Overlays.addOverlay("text3d", {
+    position: MyAvatar.position,
+    lineHeight: 0.1,
+    visible: false
+});
 
 function removeOverlays() {
     // enumerate the overlays and remove them
@@ -31,6 +36,8 @@ function removeOverlays() {
         }
     }
 
+    Overlays.deleteOverlay(textSizeOverlay);
+
     debugOverlays = {};
 }
 
@@ -60,8 +67,6 @@ function updateOverlays() {
             var overlayPosition = avatar.getJointPosition("Head");
             overlayPosition.y += 1.15;
 
-            var rows = 8;
-
             var text = avatarID + "\n"
                        +"--- Data from Mixer ---\n"
                        +"All: " + AvatarManager.getAvatarDataRate(avatarID).toFixed(2) + "kbps (" + AvatarManager.getAvatarUpdateRate(avatarID).toFixed(2) + "hz)" + "\n"
@@ -85,9 +90,11 @@ function updateOverlays() {
                        //+" SM: " + AvatarManager.getAvatarSimulationRate(avatarID,"skeletonModel").toFixed(2) + "hz \n"
                        +" JD: " + AvatarManager.getAvatarSimulationRate(avatarID,"jointData").toFixed(2) + "hz \n"
 
+            var dimensions = Overlays.textSize(textSizeOverlay, text);
             if (avatarID in debugOverlays) {
                 // keep the overlay above the current position of this avatar
                 Overlays.editOverlay(debugOverlays[avatarID][0], {
+                    dimensions: { x: 1.1 * dimensions.width, y: 0.6 * dimensions.height },
                     position: overlayPosition,
                     text: text
                 });
@@ -95,15 +102,9 @@ function updateOverlays() {
                 // add the overlay above this avatar
                 var newOverlay = Overlays.addOverlay("text3d", {
                     position: overlayPosition,
-                    dimensions: {
-                        x: 1.25,
-                        y: rows * 0.13
-                    },
+                    dimensions: { x: 1.1 * dimensions.width, y: 0.6 * dimensions.height },
                     lineHeight: 0.1,
-                    font:{size:0.1},
                     text: text,
-                    size: 1,
-                    scale: 0.4,
                     color: { red: 255, green: 255, blue: 255},
                     alpha: 1,
                     solid: true,
diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml
index 6d98e96780..d147585212 100644
--- a/scripts/developer/utilities/render/deferredLighting.qml
+++ b/scripts/developer/utilities/render/deferredLighting.qml
@@ -47,7 +47,7 @@ Rectangle {
                          "Lightmap:LightingModel:enableLightmap",
                          "Background:LightingModel:enableBackground",      
                          "Haze:LightingModel:enableHaze",                        
-                         "ssao:LightingModel:enableAmbientOcclusion",
+                         "AO:LightingModel:enableAmbientOcclusion",
                          "Textures:LightingModel:enableMaterialTexturing"                     
                     ]
                     HifiControls.CheckBox {
@@ -148,6 +148,27 @@ Rectangle {
             }
         }
         Separator {}          
+        Column {
+            anchors.left: parent.left
+            anchors.right: parent.right 
+            spacing: 5 
+            Repeater {
+                model: [ "MSAA:PrepareFramebuffer:numSamples:4:1"
+                              ]
+                ConfigSlider {
+                        label: qsTr(modelData.split(":")[0])
+                        integral: true
+                        config: render.mainViewTask.getConfig(modelData.split(":")[1])
+                        property: modelData.split(":")[2]
+                        max: modelData.split(":")[3]
+                        min: modelData.split(":")[4]
+
+                        anchors.left: parent.left
+                        anchors.right: parent.right 
+                }
+            }
+        }
+        Separator {}          
         
         Item {
             height: childrenRect.height
diff --git a/scripts/system/assets/data/createAppTooltips.json b/scripts/system/assets/data/createAppTooltips.json
index 4c78da7306..7201cdecad 100644
--- a/scripts/system/assets/data/createAppTooltips.json
+++ b/scripts/system/assets/data/createAppTooltips.json
@@ -134,6 +134,9 @@
     "bloom.bloomSize": {
         "tooltip": "The radius of bloom. The higher the value, the larger the bloom."
     },
+    "avatarPriority": {
+        "tooltip":  "Alter Avatars' update priorities."
+    },
     "modelURL": {
         "tooltip": "A mesh model from an FBX or OBJ file."
     },
diff --git a/scripts/system/away.js b/scripts/system/away.js
index a2e73ae63c..2af43b2055 100644
--- a/scripts/system/away.js
+++ b/scripts/system/away.js
@@ -45,7 +45,8 @@ var OVERLAY_DATA_HMD = {
     emissive: true,
     drawInFront: true,
     parentID: MyAvatar.SELF_ID,
-    parentJointIndex: CAMERA_MATRIX
+    parentJointIndex: CAMERA_MATRIX,
+    ignorePickIntersection: true
 };
 
 var AWAY_INTRO = {
@@ -64,7 +65,7 @@ var eventMappingName = "io.highfidelity.away"; // goActive on hand controller bu
 var eventMapping = Controller.newMapping(eventMappingName);
 var avatarPosition = MyAvatar.position;
 var wasHmdMounted = HMD.mounted;
-
+var previousBubbleState = Users.getIgnoreRadiusEnabled();
 
 // some intervals we may create/delete
 var avatarMovedInterval;
@@ -153,7 +154,7 @@ function goAway(fromStartup) {
     if (!isEnabled || isAway) {
         return;
     }
-    
+ 
     // If we're entering away mode from some other state than startup, then we create our move timer immediately.
     // However if we're just stating up, we need to delay this process so that we don't think the initial teleport
     // is actually a move.
@@ -165,7 +166,12 @@ function goAway(fromStartup) {
             avatarMovedInterval = Script.setInterval(ifAvatarMovedGoActive, BASIC_TIMER_INTERVAL);
         }, WAIT_FOR_MOVE_ON_STARTUP);
     }
-    
+
+    previousBubbleState = Users.getIgnoreRadiusEnabled();
+    if (!previousBubbleState) {
+        Users.toggleIgnoreRadius();
+    }
+    UserActivityLogger.bubbleToggled(Users.getIgnoreRadiusEnabled());
     UserActivityLogger.toggledAway(true);
     MyAvatar.isAway = true;
 }
@@ -178,6 +184,11 @@ function goActive() {
     UserActivityLogger.toggledAway(false);
     MyAvatar.isAway = false;
 
+    if (Users.getIgnoreRadiusEnabled() !== previousBubbleState) {
+        Users.toggleIgnoreRadius();
+        UserActivityLogger.bubbleToggled(Users.getIgnoreRadiusEnabled());
+    }
+
     if (!Window.hasFocus()) {
         Window.setFocus();
     }
diff --git a/scripts/system/chat.js b/scripts/system/chat.js
index b0a2e114a3..749665f3d8 100644
--- a/scripts/system/chat.js
+++ b/scripts/system/chat.js
@@ -45,6 +45,17 @@
     var speechBubbleLineHeight = 0.05; // The height of a line of text in the speech bubble.
     var SPEECH_BUBBLE_MAX_WIDTH = 1; // meters
 
+    var textSizeOverlay = Overlays.addOverlay("text3d", {
+        position: MyAvatar.position,
+        lineHeight: speechBubbleLineHeight,
+        leftMargin: 0,
+        topMargin: 0,
+        rightMargin: 0,
+        bottomMargin: 0,
+        ignoreRayIntersection: true,
+        visible: false
+    });
+
     // Load the persistent variables from the Settings, with defaults.
     function loadSettings() {
         chatName = Settings.getValue('Chat_chatName', MyAvatar.displayName);
@@ -63,6 +74,9 @@
         speechBubbleOffset = Settings.getValue('Chat_speechBubbleOffset', {x: 0.0, y: 0.3, z:0.0});
         speechBubbleJointName = Settings.getValue('Chat_speechBubbleJointName', 'Head');
         speechBubbleLineHeight = Settings.getValue('Chat_speechBubbleLineHeight', 0.05);
+        Overlays.editOverlay(textSizeOverlay, {
+            lineHeight: speechBubbleLineHeight
+        });
 
         saveSettings();
     }
@@ -637,7 +651,7 @@
         // Only overlay text3d has a way to measure the text, not entities.
         // So we make a temporary one just for measuring text, then delete it.
         var speechBubbleTextOverlayID = Overlays.addOverlay("text3d", speechBubbleParams);
-        var textSize = Overlays.textSize(speechBubbleTextOverlayID, speechBubbleMessage);
+        var textSize = Overlays.textSize(textSizeOverlay, speechBubbleMessage);
         try {
             Overlays.deleteOverlay(speechBubbleTextOverlayID);
         } catch (e) {}
@@ -971,6 +985,8 @@
         unidentifyAvatars();
         disconnectWebHandler();
 
+        Overlays.deleteOverlay(textSizeOverlay);
+
         if (onChatPage) {
             tablet.gotoHomeScreen();
             onChatPage = false;
diff --git a/scripts/system/controllers/+android_questInterface/controllerScripts.js b/scripts/system/controllers/+android_questInterface/controllerScripts.js
new file mode 100644
index 0000000000..d313efaca1
--- /dev/null
+++ b/scripts/system/controllers/+android_questInterface/controllerScripts.js
@@ -0,0 +1,58 @@
+"use strict";
+
+//  controllerScripts.js
+//
+//  Created by David Rowe on 15 Mar 2017.
+//  Copyright 2017 High Fidelity, Inc.
+//
+//  Distributed under the Apache License, Version 2.0.
+//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+/* global Script, Menu */
+
+var CONTOLLER_SCRIPTS = [
+    "squeezeHands.js",
+    "controllerDisplayManager.js",
+    "toggleAdvancedMovementForHandControllers.js",
+    "controllerDispatcher.js",
+    "controllerModules/nearParentGrabOverlay.js",
+    "controllerModules/stylusInput.js",
+    "controllerModules/equipEntity.js",
+    "controllerModules/nearTrigger.js",
+    "controllerModules/webSurfaceLaserInput.js",
+    "controllerModules/inVREditMode.js",
+    "controllerModules/disableOtherModule.js",
+    "controllerModules/farTrigger.js",
+    "controllerModules/teleport.js",
+    "controllerModules/hudOverlayPointer.js",
+    "controllerModules/scaleEntity.js",
+    "controllerModules/nearGrabHyperLinkEntity.js",
+    "controllerModules/nearTabletHighlight.js",
+    "controllerModules/nearGrabEntity.js",
+    "controllerModules/farGrabEntity.js"
+];
+
+var DEBUG_MENU_ITEM = "Debug defaultScripts.js";
+
+function runDefaultsTogether() {
+    for (var j in CONTOLLER_SCRIPTS) {
+        if (CONTOLLER_SCRIPTS.hasOwnProperty(j)) {
+            Script.include(CONTOLLER_SCRIPTS[j]);
+        }
+    }
+}
+
+function runDefaultsSeparately() {
+    for (var i in CONTOLLER_SCRIPTS) {
+        if (CONTOLLER_SCRIPTS.hasOwnProperty(i)) {
+            Script.load(CONTOLLER_SCRIPTS[i]);
+        }
+    }
+}
+
+if (Menu.isOptionChecked(DEBUG_MENU_ITEM)) {
+    runDefaultsSeparately();
+} else {
+    runDefaultsTogether();
+}
diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js
index 2a5cf5a727..28c3e2a299 100644
--- a/scripts/system/controllers/controllerDispatcher.js
+++ b/scripts/system/controllers/controllerDispatcher.js
@@ -497,6 +497,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js");
             distanceScaleEnd: true,
             hand: RIGHT_HAND
         });
+
         this.mouseRayPick = Pointers.createPointer(PickType.Ray, {
             joint: "Mouse",
             filter: Picks.PICK_OVERLAYS | Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_NONCOLLIDABLE,
diff --git a/scripts/system/controllers/controllerModules/farGrabEntity.js b/scripts/system/controllers/controllerModules/farGrabEntity.js
index 197a809e91..65a3671cae 100644
--- a/scripts/system/controllers/controllerModules/farGrabEntity.js
+++ b/scripts/system/controllers/controllerModules/farGrabEntity.js
@@ -60,6 +60,14 @@ Script.include("/~/system/libraries/controllers.js");
         this.reticleMaxY = 0;
         this.endedGrab = 0;
         this.MIN_HAPTIC_PULSE_INTERVAL = 500; // ms
+        this.disabled = false;
+        var _this = this;
+        this.leftTrigger = 0.0;
+        this.rightTrigger = 0.0;
+        this.initialControllerRotation = Quat.IDENTITY;
+        this.currentControllerRotation = Quat.IDENTITY;
+        this.manipulating = false;
+        this.wasManipulating = false;
 
         var FAR_GRAB_JOINTS = [65527, 65528]; // FARGRAB_LEFTHAND_INDEX, FARGRAB_RIGHTHAND_INDEX
 
@@ -75,6 +83,45 @@ Script.include("/~/system/libraries/controllers.js");
             100,
             makeLaserParams(this.hand, false));
 
+        this.getOtherModule = function () {
+            return getEnabledModuleByName(this.hand === RIGHT_HAND ? ("LeftFarGrabEntity") : ("RightFarGrabEntity"));
+        };
+
+        // Get the rotation of the fargrabbed entity.
+        this.getTargetRotation = function () {
+            if (this.targetIsNull()) {
+                return null;
+            } else {
+                var props = Entities.getEntityProperties(this.targetEntityID, ["rotation"]);
+                return props.rotation;
+            }
+        };
+
+        this.getOffhand = function () {
+            return (this.hand === RIGHT_HAND ? LEFT_HAND : RIGHT_HAND);
+        }
+
+        this.getOffhandTrigger = function () {
+            return (_this.hand === RIGHT_HAND ? _this.leftTrigger : _this.rightTrigger);
+        }
+
+        // Activation criteria for rotating a fargrabbed entity. If we're changing the mapping, this is where to do it.
+        this.shouldManipulateTarget = function () {
+            return (_this.getOffhandTrigger() > TRIGGER_ON_VALUE) ? true : false;
+        };
+
+        // Get the delta between the current rotation and where the controller was when manipulation started.
+        this.calculateEntityRotationManipulation = function (controllerRotation) {
+            return Quat.multiply(controllerRotation, Quat.inverse(this.initialControllerRotation));
+        };
+
+        this.setJointTranslation = function (newTargetPosLocal) {
+            MyAvatar.setJointTranslation(FAR_GRAB_JOINTS[this.hand], newTargetPosLocal);
+        };
+
+        this.setJointRotation = function (newTargetRotLocal) {
+            MyAvatar.setJointRotation(FAR_GRAB_JOINTS[this.hand], newTargetRotLocal);
+        };
 
         this.handToController = function() {
             return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
@@ -142,8 +189,9 @@ Script.include("/~/system/libraries/controllers.js");
             Messages.sendLocalMessage('Hifi-unhighlight-entity', JSON.stringify(message));
 
             var newTargetPosLocal = MyAvatar.worldToJointPoint(targetProps.position);
-            MyAvatar.setJointTranslation(FAR_GRAB_JOINTS[this.hand], newTargetPosLocal);
-            MyAvatar.setJointRotation(FAR_GRAB_JOINTS[this.hand], { x: 0, y: 0, z: 0, w: 1 });
+            var newTargetRotLocal = targetProps.rotation;
+            this.setJointTranslation(newTargetPosLocal);
+            this.setJointRotation(newTargetRotLocal);
 
             var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
             Entities.callEntityMethod(targetProps.id, "startDistanceGrab", args);
@@ -227,12 +275,44 @@ Script.include("/~/system/libraries/controllers.js");
             newTargetPosition = Vec3.sum(newTargetPosition, worldControllerPosition);
             newTargetPosition = Vec3.sum(newTargetPosition, this.offsetPosition);
 
-            // MyAvatar.setJointTranslation(FAR_GRAB_JOINTS[this.hand], MyAvatar.worldToJointPoint(newTargetPosition));
-
-            // var newTargetPosLocal = Mat4.transformPoint(MyAvatar.getSensorToWorldMatrix(), newTargetPosition);
             var newTargetPosLocal = MyAvatar.worldToJointPoint(newTargetPosition);
-            MyAvatar.setJointTranslation(FAR_GRAB_JOINTS[this.hand], newTargetPosLocal);
-            MyAvatar.setJointRotation(FAR_GRAB_JOINTS[this.hand], { x: 0, y: 0, z: 0, w: 1 });
+
+            // This block handles the user's ability to rotate the object they're FarGrabbing
+            if (this.shouldManipulateTarget()) {
+                // Get the pose of the controller that is not grabbing.
+                var pose = Controller.getPoseValue((this.getOffhand() ? Controller.Standard.RightHand : Controller.Standard.LeftHand));
+                if (pose.valid) {
+                    // If we weren't manipulating the object yet, initialize the entity's original position.
+                    if (!this.manipulating) {
+                        // This will only be triggered if we've let go of the off-hand trigger and pulled it again without ending a grab.
+                        // Need to poll the entity's rotation again here.
+                        if (!this.wasManipulating) {
+                            this.initialEntityRotation = this.getTargetRotation();
+                        }
+                        // Save the original controller orientation, we only care about the delta between this rotation and wherever
+                        // the controller rotates, so that we can apply it to the entity's rotation.
+                        this.initialControllerRotation = Quat.multiply(pose.rotation, MyAvatar.orientation);
+                        this.manipulating = true;
+                    }
+                }
+
+                var rot = Quat.multiply(pose.rotation, MyAvatar.orientation);
+                var rotBetween = this.calculateEntityRotationManipulation(rot);
+                var doubleRot = Quat.multiply(rotBetween, rotBetween);
+                this.lastJointRotation = Quat.multiply(doubleRot, this.initialEntityRotation);
+                this.setJointRotation(this.lastJointRotation);
+            } else {
+                // If we were manipulating but the user isn't currently expressing this intent, we want to know so we preserve the rotation
+                // between manipulations without ending the fargrab.
+                if (this.manipulating) {
+                    this.initialEntityRotation = this.lastJointRotation;
+                    this.wasManipulating = true;
+                }
+                this.manipulating = false;
+                // Reset the inital controller position.
+                this.initialControllerRotation = Quat.IDENTITY;
+            }
+            this.setJointTranslation(newTargetPosLocal);
 
             this.previousRoomControllerPosition = roomControllerPosition;
         };
@@ -254,9 +334,15 @@ Script.include("/~/system/libraries/controllers.js");
             }));
             unhighlightTargetEntity(this.targetEntityID);
             this.grabbing = false;
-            this.targetEntityID = null;
             this.potentialEntityWithContextOverlay = false;
             MyAvatar.clearJointData(FAR_GRAB_JOINTS[this.hand]);
+            this.initialEntityRotation = Quat.IDENTITY;
+            this.initialControllerRotation = Quat.IDENTITY;
+            this.targetEntityID = null;
+            this.manipulating = false;
+            this.wasManipulating = false;
+            var otherModule = this.getOtherModule();
+            otherModule.disabled = false;
         };
 
         this.updateRecommendedArea = function() {
@@ -326,7 +412,9 @@ Script.include("/~/system/libraries/controllers.js");
 
                 this.distanceHolding = false;
 
-                if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE) {
+                if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE && !this.disabled) {
+                    var otherModule = this.getOtherModule();
+                    otherModule.disabled = true;
                     return makeRunningValues(true, [], []);
                 } else {
                     this.destroyContextOverlay();
@@ -336,6 +424,8 @@ Script.include("/~/system/libraries/controllers.js");
         };
 
         this.run = function (controllerData) {
+            this.leftTrigger = controllerData.triggerValues[LEFT_HAND];
+            this.rightTrigger = controllerData.triggerValues[RIGHT_HAND];
             if (controllerData.triggerValues[this.hand] < TRIGGER_OFF_VALUE || this.targetIsNull()) {
                 this.endFarGrabEntity(controllerData);
                 return makeRunningValues(false, [], []);
diff --git a/scripts/system/controllers/controllerModules/hudOverlayPointer.js b/scripts/system/controllers/controllerModules/hudOverlayPointer.js
index efbca66d72..f7d5b5a2dd 100644
--- a/scripts/system/controllers/controllerModules/hudOverlayPointer.js
+++ b/scripts/system/controllers/controllerModules/hudOverlayPointer.js
@@ -30,6 +30,20 @@
             100,
             makeLaserParams((this.hand + HUD_LASER_OFFSET), false));
 
+        this.getFarGrab = function () {
+            return getEnabledModuleByName(this.hand === RIGHT_HAND ? ("RightFarGrabEntity") : ("LeftFarGrabEntity"));
+        }
+
+        this.farGrabActive = function () {
+            var farGrab = this.getFarGrab();
+            // farGrab will be null if module isn't loaded.
+            if (farGrab) {
+                return farGrab.targetIsNull();
+            } else {
+                return false;
+            }
+        };
+
         this.getOtherHandController = function() {
             return (this.hand === RIGHT_HAND) ? Controller.Standard.LeftHand : Controller.Standard.RightHand;
         };
@@ -79,7 +93,7 @@
 
         this.isReady = function (controllerData) {
             var otherModuleRunning = this.getOtherModule().running;
-            if (!otherModuleRunning && HMD.active) {
+            if (!otherModuleRunning && HMD.active && !this.farGrabActive()) {
                 if (this.processLaser(controllerData)) {
                     this.running = true;
                     return ControllerDispatcherUtils.makeRunningValues(true, [], []);
diff --git a/scripts/system/controllers/controllerModules/nearGrabEntity.js b/scripts/system/controllers/controllerModules/nearGrabEntity.js
index 0f8071677c..763c1a1ce0 100644
--- a/scripts/system/controllers/controllerModules/nearGrabEntity.js
+++ b/scripts/system/controllers/controllerModules/nearGrabEntity.js
@@ -134,7 +134,7 @@ Script.include("/~/system/libraries/controllers.js");
 
             var scaleModuleName = this.hand === RIGHT_HAND ? "RightScaleEntity" : "LeftScaleEntity";
             var scaleModule = getEnabledModuleByName(scaleModuleName);
-            if (scaleModule.grabbedThingID || scaleModule.isReady(controllerData).active) {
+            if (scaleModule && (scaleModule.grabbedThingID || scaleModule.isReady(controllerData).active)) {
                 // we're rescaling -- don't start a grab.
                 return makeRunningValues(false, [], []);
             }
diff --git a/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js b/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js
index e59b2e35ad..5dcfee23cb 100644
--- a/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js
+++ b/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js
@@ -117,11 +117,12 @@ Script.include("/~/system/libraries/utils.js");
                 this.previousParentID[this.grabbedThingID] = grabbedProperties.parentID;
                 this.previousParentJointIndex[this.grabbedThingID] = grabbedProperties.parentJointIndex;
             }
-            Overlays.editOverlay(this.grabbedThingID, reparentProps);
 
             // resizeTablet to counter adjust offsets to account for change of scale from sensorToWorldMatrix
             if (HMD.tabletID && this.grabbedThingID === HMD.tabletID) {
-                resizeTablet(getTabletWidthFromSettings(), reparentProps.parentJointIndex);
+                reparentAndScaleTablet(getTabletWidthFromSettings(), reparentProps);
+            } else {
+                Entities.editEntity(this.grabbedThingID, reparentProps);
             }
 
             Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
@@ -140,7 +141,7 @@ Script.include("/~/system/libraries/utils.js");
                 });
             } else if (!this.robbed){
                 // before we grabbed it, overlay was a child of something; put it back.
-                Overlays.editOverlay(this.grabbedThingID, {
+                Entities.editEntity(this.grabbedThingID, {
                     parentID: this.previousParentID[this.grabbedThingID],
                     parentJointIndex: this.previousParentJointIndex[this.grabbedThingID]
                 });
diff --git a/scripts/system/controllers/controllerModules/teleport.js b/scripts/system/controllers/controllerModules/teleport.js
index 8770ae8dde..23457cdd85 100644
--- a/scripts/system/controllers/controllerModules/teleport.js
+++ b/scripts/system/controllers/controllerModules/teleport.js
@@ -196,14 +196,14 @@ Script.include("/~/system/libraries/controllers.js");
 
             var playAreaOverlayProperties = {
                 dimensions:
-                    Vec3.multiply(this.teleportScaleFactor * avatarScale, {
-                        x: this.playArea.width,
-                        y: this.PLAY_AREA_OVERLAY_MODEL_DIMENSIONS.y,
-                        z: this.playArea.height
+                    Vec3.multiply(_this.teleportScaleFactor * avatarScale, {
+                        x: _this.playArea.width,
+                        y: _this.PLAY_AREA_OVERLAY_MODEL_DIMENSIONS.y,
+                        z: _this.playArea.height
                     })
             };
 
-            if (this.teleportScaleFactor < 1) {
+            if (_this.teleportScaleFactor < 1) {
                 // Adjust position of playAreOverlay so that its base is at correct height.
                 // Always parenting to teleport target is good enough for this.
                 var sensorToWorldMatrix = MyAvatar.sensorToWorldMatrix;
@@ -212,37 +212,37 @@ Script.include("/~/system/libraries/controllers.js");
                 var avatarSensorPosition = Mat4.transformPoint(worldToSensorMatrix, MyAvatar.position);
                 avatarSensorPosition.y = 0;
 
-                var targetRotation = Overlays.getProperty(this.targetOverlayID, "rotation");
+                var targetRotation = Overlays.getProperty(_this.targetOverlayID, "rotation");
                 var relativePlayAreaCenterOffset =
-                    Vec3.sum(this.playAreaCenterOffset, { x: 0, y: -TARGET_MODEL_DIMENSIONS.y / 2, z: 0 });
+                    Vec3.sum(_this.playAreaCenterOffset, { x: 0, y: -TARGET_MODEL_DIMENSIONS.y / 2, z: 0 });
                 var localPosition = Vec3.multiplyQbyV(Quat.inverse(targetRotation),
                     Vec3.multiplyQbyV(sensorToWorldRotation,
                         Vec3.multiply(avatarScale, Vec3.subtract(relativePlayAreaCenterOffset, avatarSensorPosition))));
-                localPosition.y = this.teleportScaleFactor * localPosition.y;
+                localPosition.y = _this.teleportScaleFactor * localPosition.y;
 
-                playAreaOverlayProperties.parentID = this.targetOverlayID;
+                playAreaOverlayProperties.parentID = _this.targetOverlayID;
                 playAreaOverlayProperties.localPosition = localPosition;
             }
 
-            Overlays.editOverlay(this.playAreaOverlay, playAreaOverlayProperties);
+            Overlays.editOverlay(_this.playAreaOverlay, playAreaOverlayProperties);
 
-            for (var i = 0; i < this.playAreaSensorPositionOverlays.length; i++) {
-                localPosition = this.playAreaSensorPositions[i];
+            for (var i = 0; i < _this.playAreaSensorPositionOverlays.length; i++) {
+                localPosition = _this.playAreaSensorPositions[i];
                 localPosition = Vec3.multiply(avatarScale, localPosition);
                 // Position relative to the play area.
-                localPosition.y = avatarScale * (this.PLAY_AREA_SENSOR_OVERLAY_DIMENSIONS.y / 2
-                    - this.PLAY_AREA_OVERLAY_MODEL_DIMENSIONS.y / 2);
-                Overlays.editOverlay(this.playAreaSensorPositionOverlays[i], {
-                    dimensions: Vec3.multiply(this.teleportScaleFactor * avatarScale, this.PLAY_AREA_SENSOR_OVERLAY_DIMENSIONS),
-                    parentID: this.playAreaOverlay,
+                localPosition.y = avatarScale * (_this.PLAY_AREA_SENSOR_OVERLAY_DIMENSIONS.y / 2
+                    - _this.PLAY_AREA_OVERLAY_MODEL_DIMENSIONS.y / 2);
+                Overlays.editOverlay(_this.playAreaSensorPositionOverlays[i], {
+                    dimensions: Vec3.multiply(_this.teleportScaleFactor * avatarScale, _this.PLAY_AREA_SENSOR_OVERLAY_DIMENSIONS),
+                    parentID: _this.playAreaOverlay,
                     localPosition: localPosition
                 });
             }
         };
 
         this.updatePlayAreaScale = function () {
-            if (this.isPlayAreaAvailable) {
-                this.setPlayAreaDimensions();
+            if (_this.isPlayAreaAvailable) {
+                _this.setPlayAreaDimensions();
             }
         };
 
@@ -265,7 +265,7 @@ Script.include("/~/system/libraries/controllers.js");
             for (var i = 0, length = teleportRenderStates.length; i < length; i++) {
                 var state = properties.renderStates[teleportRenderStates[i].name];
                 if (state && state.end) {
-                    Selection.addToSelectedItemsList(this.teleporterSelectionName, "overlay", state.end);
+                    Selection.addToSelectedItemsList(_this.teleporterSelectionName, "overlay", state.end);
                 }
             }
         };
@@ -448,34 +448,34 @@ Script.include("/~/system/libraries/controllers.js");
         this.translateZAction = Controller.findAction("TranslateZ");
 
         this.setPlayAreaVisible = function (visible, targetOverlayID, fade) {
-            if (!this.isPlayAreaAvailable || this.isPlayAreaVisible === visible) {
+            if (!_this.isPlayAreaAvailable || _this.isPlayAreaVisible === visible) {
                 return;
             }
 
-            this.wasPlayAreaVisible = this.isPlayAreaVisible;
-            this.isPlayAreaVisible = visible;
-            this.targetOverlayID = targetOverlayID;
+            _this.wasPlayAreaVisible = _this.isPlayAreaVisible;
+            _this.isPlayAreaVisible = visible;
+            _this.targetOverlayID = targetOverlayID;
 
-            if (this.teleportedFadeTimer !== null) {
-                Script.clearTimeout(this.teleportedFadeTimer);
-                this.teleportedFadeTimer = null;
+            if (_this.teleportedFadeTimer !== null) {
+                Script.clearTimeout(_this.teleportedFadeTimer);
+                _this.teleportedFadeTimer = null;
             }
             if (visible || !fade) {
                 // Immediately make visible or invisible.
-                this.isPlayAreaVisible = visible;
-                Overlays.editOverlay(this.playAreaOverlay, {
+                _this.isPlayAreaVisible = visible;
+                Overlays.editOverlay(_this.playAreaOverlay, {
                     dimensions: Vec3.ZERO,
-                    alpha: this.PLAY_AREA_BOX_ALPHA,
+                    alpha: _this.PLAY_AREA_BOX_ALPHA,
                     visible: visible
                 });
-                for (var i = 0; i < this.playAreaSensorPositionOverlays.length; i++) {
-                    Overlays.editOverlay(this.playAreaSensorPositionOverlays[i], {
+                for (var i = 0; i < _this.playAreaSensorPositionOverlays.length; i++) {
+                    Overlays.editOverlay(_this.playAreaSensorPositionOverlays[i], {
                         dimensions: Vec3.ZERO,
-                        alpha: this.PLAY_AREA_SENSOR_ALPHA,
+                        alpha: _this.PLAY_AREA_SENSOR_ALPHA,
                         visible: visible
                     });
                 }
-                Overlays.editOverlay(this.teleportedTargetOverlay, { visible: false });
+                Overlays.editOverlay(_this.teleportedTargetOverlay, { visible: false });
             } else {
                 // Fading out of overlays is initiated in setTeleportVisible().
             }
@@ -494,22 +494,22 @@ Script.include("/~/system/libraries/controllers.js");
             var MIN_PARENTING_DISTANCE = 0.2; // Parenting under this distance results in the play area's rotation jittering.
             if (Vec3.distance(targetXZPosition, avatarXZPosition) < MIN_PARENTING_DISTANCE) {
                 // Set play area position and rotation in world coordinates with no parenting.
-                Overlays.editOverlay(this.playAreaOverlay, {
+                Overlays.editOverlay(_this.playAreaOverlay, {
                     parentID: Uuid.NULL,
                     position: Vec3.sum(position,
                         Vec3.multiplyQbyV(sensorToWorldRotation,
                             Vec3.multiply(MyAvatar.sensorToWorldScale,
-                                Vec3.subtract(this.playAreaCenterOffset, avatarSensorPosition)))),
+                                Vec3.subtract(_this.playAreaCenterOffset, avatarSensorPosition)))),
                     rotation: sensorToWorldRotation
                 });
             } else {
                 // Set play area position and rotation in local coordinates with parenting.
-                var targetRotation = Overlays.getProperty(this.targetOverlayID, "rotation");
+                var targetRotation = Overlays.getProperty(_this.targetOverlayID, "rotation");
                 var sensorToTargetRotation = Quat.multiply(Quat.inverse(targetRotation), sensorToWorldRotation);
                 var relativePlayAreaCenterOffset =
-                    Vec3.sum(this.playAreaCenterOffset, { x: 0, y: -TARGET_MODEL_DIMENSIONS.y / 2, z: 0 });
-                Overlays.editOverlay(this.playAreaOverlay, {
-                    parentID: this.targetOverlayID,
+                    Vec3.sum(_this.playAreaCenterOffset, { x: 0, y: -TARGET_MODEL_DIMENSIONS.y / 2, z: 0 });
+                Overlays.editOverlay(_this.playAreaOverlay, {
+                    parentID: _this.targetOverlayID,
                     localPosition: Vec3.multiplyQbyV(Quat.inverse(targetRotation),
                         Vec3.multiplyQbyV(sensorToWorldRotation,
                             Vec3.multiply(MyAvatar.sensorToWorldScale,
@@ -578,33 +578,33 @@ Script.include("/~/system/libraries/controllers.js");
                     }
                 }
                 _this.teleportedFadeTimer = null;
-                Selection.disableListHighlight(this.teleporterSelectionName);
+                Selection.disableListHighlight(_this.teleporterSelectionName);
             }
         };
 
         this.cancelFade = function () {
             // Other hand may call this to immediately hide fading overlays.
             var i, length;
-            if (this.teleportedFadeTimer) {
-                Overlays.editOverlay(this.teleportedTargetOverlay, { visible: false });
-                if (this.wasPlayAreaVisible) {
-                    Overlays.editOverlay(this.playAreaOverlay, { visible: false });
-                    for (i = 0, length = this.playAreaSensorPositionOverlays.length; i < length; i++) {
-                        Overlays.editOverlay(this.playAreaSensorPositionOverlays[i], { visible: false });
+            if (_this.teleportedFadeTimer) {
+                Overlays.editOverlay(_this.teleportedTargetOverlay, { visible: false });
+                if (_this.wasPlayAreaVisible) {
+                    Overlays.editOverlay(_this.playAreaOverlay, { visible: false });
+                    for (i = 0, length = _this.playAreaSensorPositionOverlays.length; i < length; i++) {
+                        Overlays.editOverlay(_this.playAreaSensorPositionOverlays[i], { visible: false });
                     }
                 }
-                this.teleportedFadeTimer = null;
+                _this.teleportedFadeTimer = null;
             }
         };
 
         this.setTeleportVisible = function (visible, mode, fade) {
             // Scales in teleport target and play area when start displaying them.
-            if (visible === this.isTeleportVisible) {
+            if (visible === _this.isTeleportVisible) {
                 return;
             }
 
             if (visible) {
-                this.teleportScaleMode = mode;
+                _this.teleportScaleMode = mode;
                 Pointers.editRenderState(
                     mode === "head" ? _this.teleportParabolaHeadVisuals : _this.teleportParabolaHandVisuals,
                     "teleport",
@@ -613,42 +613,42 @@ Script.include("/~/system/libraries/controllers.js");
                         end: { dimensions: Vec3.ZERO }
                     }
                 );
-                this.getOtherModule().cancelFade();
-                this.teleportScaleStart = Date.now();
-                this.teleportScaleFactor = 0;
-                this.scaleInTeleport();
-                Selection.enableListHighlight(this.teleporterSelectionName, this.TELEPORTER_SELECTION_STYLE);
+                _this.getOtherModule().cancelFade();
+                _this.teleportScaleStart = Date.now();
+                _this.teleportScaleFactor = 0;
+                _this.scaleInTeleport();
+                Selection.enableListHighlight(_this.teleporterSelectionName, _this.TELEPORTER_SELECTION_STYLE);
             } else {
-                if (this.teleportScaleTimer !== null) {
-                    Script.clearTimeout(this.teleportScaleTimer);
-                    this.teleportScaleTimer = null;
+                if (_this.teleportScaleTimer !== null) {
+                    Script.clearTimeout(_this.teleportScaleTimer);
+                    _this.teleportScaleTimer = null;
                 }
 
                 if (fade) {
                     // Copy of target at teleported position for fading.
                     var avatarScale = MyAvatar.sensorToWorldScale;
-                    Overlays.editOverlay(this.teleportedTargetOverlay, {
-                        position: Vec3.sum(this.teleportedPosition, {
+                    Overlays.editOverlay(_this.teleportedTargetOverlay, {
+                        position: Vec3.sum(_this.teleportedPosition, {
                             x: 0,
                             y: -getAvatarFootOffset() + avatarScale * TARGET_MODEL_DIMENSIONS.y / 2,
                             z: 0
                         }),
-                        rotation: Quat.multiply(this.TELEPORTED_TARGET_ROTATION, MyAvatar.orientation),
+                        rotation: Quat.multiply(_this.TELEPORTED_TARGET_ROTATION, MyAvatar.orientation),
                         dimensions: Vec3.multiply(avatarScale, TARGET_MODEL_DIMENSIONS),
-                        alpha: this.TELEPORTED_TARGET_ALPHA,
+                        alpha: _this.TELEPORTED_TARGET_ALPHA,
                         visible: true
                     });
 
                     // Fade out over time.
-                    this.teleportedFadeDelayFactor = 1.0;
-                    this.teleportedFadeFactor = 1.0;
-                    this.teleportedFadeTimer = Script.setTimeout(this.fadeOutTeleport, this.TELEPORTED_FADE_DELAY);
+                    _this.teleportedFadeDelayFactor = 1.0;
+                    _this.teleportedFadeFactor = 1.0;
+                    _this.teleportedFadeTimer = Script.setTimeout(_this.fadeOutTeleport, _this.TELEPORTED_FADE_DELAY);
                 } else {
-                    Selection.disableListHighlight(this.teleporterSelectionName);
+                    Selection.disableListHighlight(_this.teleporterSelectionName);
                 }
             }
 
-            this.isTeleportVisible = visible;
+            _this.isTeleportVisible = visible;
         };
 
 
@@ -697,7 +697,7 @@ Script.include("/~/system/libraries/controllers.js");
             100);
 
         this.enterTeleport = function() {
-            this.state = TELEPORTER_STATES.TARGETTING;
+            _this.state = TELEPORTER_STATES.TARGETTING;
         };
 
         this.isReady = function(controllerData, deltaTime) {
@@ -761,23 +761,23 @@ Script.include("/~/system/libraries/controllers.js");
 
             if (teleportLocationType === TARGET.NONE) {
                 // Use the cancel default state
-                this.setTeleportState(mode, "cancel", "");
+                _this.setTeleportState(mode, "cancel", "");
             } else if (teleportLocationType === TARGET.INVALID) {
-                this.setTeleportState(mode, "", "cancel");
+                _this.setTeleportState(mode, "", "cancel");
             } else if (teleportLocationType === TARGET.COLLIDES) {
-                this.setTeleportState(mode, "cancel", "collision");
+                _this.setTeleportState(mode, "cancel", "collision");
             } else if (teleportLocationType === TARGET.SURFACE || teleportLocationType === TARGET.DISCREPANCY) {
-                this.setTeleportState(mode, "teleport", "collision");
-                this.updatePlayArea(result.intersection);
+                _this.setTeleportState(mode, "teleport", "collision");
+                _this.updatePlayArea(result.intersection);
             } else if (teleportLocationType === TARGET.SEAT) {
-                this.setTeleportState(mode, "collision", "seat");
+                _this.setTeleportState(mode, "collision", "seat");
             }
-            return this.teleport(result, teleportLocationType);
+            return _this.teleport(result, teleportLocationType);
         };
 
         this.teleport = function(newResult, target) {
             var result = newResult;
-            this.teleportedPosition = newResult.intersection;
+            _this.teleportedPosition = newResult.intersection;
             if (_this.buttonValue !== 0) {
                 return makeRunningValues(true, [], []);
             }
@@ -795,14 +795,14 @@ Script.include("/~/system/libraries/controllers.js");
                 MyAvatar.centerBody();
             }
 
-            this.disableLasers();
-            this.active = false;
+            _this.disableLasers();
+            _this.active = false;
             return makeRunningValues(false, [], []);
         };
 
         this.disableLasers = function() {
-            this.setPlayAreaVisible(false, null, true);
-            this.setTeleportVisible(false, null, true);
+            _this.setPlayAreaVisible(false, null, true);
+            _this.setTeleportVisible(false, null, true);
             Pointers.disablePointer(_this.teleportParabolaHandVisuals);
             Pointers.disablePointer(_this.teleportParabolaHandCollisions);
             Pointers.disablePointer(_this.teleportParabolaHeadVisuals);
@@ -815,10 +815,10 @@ Script.include("/~/system/libraries/controllers.js");
 
         this.setTeleportState = function (mode, visibleState, invisibleState) {
             var teleportState = mode + visibleState + invisibleState;
-            if (teleportState === this.teleportState) {
+            if (teleportState === _this.teleportState) {
                 return;
             }
-            this.teleportState = teleportState;
+            _this.teleportState = teleportState;
 
             var pointerID;
             if (mode === 'head') {
@@ -831,16 +831,16 @@ Script.include("/~/system/libraries/controllers.js");
                 pointerID = _this.teleportParabolaHandVisuals;
             }
             var visible = visibleState === "teleport";
-            this.setPlayAreaVisible(visible && MyAvatar.showPlayArea,
+            _this.setPlayAreaVisible(visible && MyAvatar.showPlayArea,
                 Pointers.getPointerProperties(pointerID).renderStates.teleport.end, false);
-            this.setTeleportVisible(visible, mode, false);
+            _this.setTeleportVisible(visible, mode, false);
         };
 
         this.setIgnoreEntities = function(entitiesToIgnore) {
-            Pointers.setIgnoreItems(this.teleportParabolaHandVisuals, entitiesToIgnore);
-            Pointers.setIgnoreItems(this.teleportParabolaHandCollisions, entitiesToIgnore);
-            Pointers.setIgnoreItems(this.teleportParabolaHeadVisuals, entitiesToIgnore);
-            Pointers.setIgnoreItems(this.teleportParabolaHeadCollisions, entitiesToIgnore);
+            Pointers.setIgnoreItems(_this.teleportParabolaHandVisuals, entitiesToIgnore);
+            Pointers.setIgnoreItems(_this.teleportParabolaHandCollisions, entitiesToIgnore);
+            Pointers.setIgnoreItems(_this.teleportParabolaHeadVisuals, entitiesToIgnore);
+            Pointers.setIgnoreItems(_this.teleportParabolaHeadCollisions, entitiesToIgnore);
             Picks.setIgnoreItems(_this.teleportHeadCollisionPick, entitiesToIgnore);
             Picks.setIgnoreItems(_this.teleportHandCollisionPick, entitiesToIgnore);
         };
diff --git a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js
index ec35dfe081..cf700a8ad9 100644
--- a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js
+++ b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js
@@ -37,6 +37,20 @@ Script.include("/~/system/libraries/controllers.js");
             100,
             makeLaserParams(hand, true));
 
+        this.getFarGrab = function () {
+            return getEnabledModuleByName(this.hand === RIGHT_HAND ? ("RightFarGrabEntity") : ("LeftFarGrabEntity"));
+        };
+
+        this.farGrabActive = function () {
+            var farGrab = this.getFarGrab();
+            // farGrab will be null if module isn't loaded.
+            if (farGrab) {
+                return farGrab.targetIsNull();
+            } else {
+                return false;
+            }
+        };
+
         this.grabModuleWantsNearbyOverlay = function(controllerData) {
             if (controllerData.triggerValues[this.hand] > TRIGGER_ON_VALUE || controllerData.secondaryValues[this.hand] > BUMPER_ON_VALUE) {
                 var nearGrabName = this.hand === RIGHT_HAND ? "RightNearParentingGrabOverlay" : "LeftNearParentingGrabOverlay";
@@ -184,7 +198,12 @@ Script.include("/~/system/libraries/controllers.js");
 
         this.dominantHandOverride = false;
 
-        this.isReady = function(controllerData) {
+        this.isReady = function (controllerData) {
+            // Trivial rejection for when FarGrab is active.
+            if (this.farGrabActive()) {
+                return makeRunningValues(false, [], []);
+            }
+
             var isTriggerPressed = controllerData.triggerValues[this.hand] > TRIGGER_OFF_VALUE &&
                                    controllerData.triggerValues[this.otherHand] <= TRIGGER_OFF_VALUE;
             var type = this.getInteractableType(controllerData, isTriggerPressed, false);
@@ -228,8 +247,8 @@ Script.include("/~/system/libraries/controllers.js");
 
         this.run = function(controllerData, deltaTime) {
             this.addObjectToIgnoreList(controllerData);
-            var type = this.getInteractableType(controllerData, isTriggerPressed, false);
             var isTriggerPressed = controllerData.triggerValues[this.hand] > TRIGGER_OFF_VALUE;
+            var type = this.getInteractableType(controllerData, isTriggerPressed, false);
             var laserOn = isTriggerPressed || this.parameters.handLaser.alwaysOn;
             this.addObjectToIgnoreList(controllerData);
 
diff --git a/scripts/system/controllers/controllerScripts.js b/scripts/system/controllers/controllerScripts.js
index 86ff7701c3..726e075fcc 100644
--- a/scripts/system/controllers/controllerScripts.js
+++ b/scripts/system/controllers/controllerScripts.js
@@ -30,7 +30,6 @@ var CONTOLLER_SCRIPTS = [
     "controllerModules/teleport.js",
     "controllerModules/hudOverlayPointer.js",
     "controllerModules/mouseHMD.js",
-    "controllerModules/scaleEntity.js",
     "controllerModules/nearGrabHyperLinkEntity.js",
     "controllerModules/nearTabletHighlight.js",
     "controllerModules/nearGrabEntity.js",
diff --git a/scripts/system/edit.js b/scripts/system/edit.js
index 9d807264aa..2c3785217c 100644
--- a/scripts/system/edit.js
+++ b/scripts/system/edit.js
@@ -382,7 +382,8 @@ const DEFAULT_ENTITY_PROPERTIES = {
             },
         },
         shapeType: "box",
-        bloomMode: "inherit"
+        bloomMode: "inherit",
+        avatarPriority: "inherit"
     },
     Model: {
         collisionShape: "none",
diff --git a/scripts/system/html/js/SnapshotReview.js b/scripts/system/html/js/SnapshotReview.js
index 91866605a4..1e8be9d644 100644
--- a/scripts/system/html/js/SnapshotReview.js
+++ b/scripts/system/html/js/SnapshotReview.js
@@ -450,7 +450,7 @@ function updateShareInfo(containerID, storyID) {
     facebookButton.setAttribute("href", 'https://www.facebook.com/dialog/feed?app_id=1585088821786423&link=' + shareURL);
 
     twitterButton.setAttribute("target", "_blank");
-    twitterButton.setAttribute("href", 'https://twitter.com/intent/tweet?text=I%20just%20took%20a%20snapshot!&url=' + shareURL + '&via=highfidelityinc&hashtags=VR,HiFi');
+    twitterButton.setAttribute("href", 'https://twitter.com/intent/tweet?text=I%20just%20took%20a%20snapshot!&url=' + shareURL + '&via=highfidelityVR&hashtags=VR,HiFi');
 
     hideUploadingMessageAndMaybeShare(containerID, storyID);
 }
diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js
index c1a8f363b5..863168d7fd 100644
--- a/scripts/system/html/js/entityProperties.js
+++ b/scripts/system/html/js/entityProperties.js
@@ -428,6 +428,13 @@ const GROUPS = [
                 propertyID: "bloom.bloomSize",
                 showPropertyRule: { "bloomMode": "enabled" },
             },
+            {
+                label: "Avatar Priority",
+                type: "dropdown",
+                options: { inherit: "Inherit", crowd: "Crowd", hero: "Hero" },
+                propertyID: "avatarPriority",
+            },
+
         ]
     },
     {
@@ -1485,6 +1492,8 @@ const ENTITY_SCRIPT_STATUS = {
     unloaded: "Unloaded"
 };
 
+const ENABLE_DISABLE_SELECTOR = "input, textarea, span, .dropdown dl, .color-picker";
+
 const PROPERTY_NAME_DIVISION = {
     GROUP: 0,
     PROPERTY: 1,
@@ -1584,8 +1593,7 @@ function disableChildren(el, selector) {
 }
 
 function enableProperties() {
-    enableChildren(document.getElementById("properties-list"),
-                   "input, textarea, checkbox, .dropdown dl, .color-picker , .draggable-number.text");
+    enableChildren(document.getElementById("properties-list"), ENABLE_DISABLE_SELECTOR);
     enableChildren(document, ".colpick");
     
     let elLocked = getPropertyInputElement("locked");
@@ -1596,8 +1604,7 @@ function enableProperties() {
 }
 
 function disableProperties() {
-    disableChildren(document.getElementById("properties-list"),
-                    "input, textarea, checkbox, .dropdown dl, .color-picker, .draggable-number.text");
+    disableChildren(document.getElementById("properties-list"), ENABLE_DISABLE_SELECTOR);
     disableChildren(document, ".colpick");
     for (let pickKey in colorPickers) {
         colorPickers[pickKey].colpickHide();
@@ -3349,8 +3356,8 @@ function loaded() {
                                 let shouldHide = selectedEntityProperties.certificateID !== "";
                                 if (shouldHide) {
                                     propertyValue = "** Certified **";
+                                    property.elInput.disabled = true;
                                 }
-                                property.elInput.disabled = shouldHide;
                             }
                             
                             let isPropertyNotNumber = false;
diff --git a/scripts/system/interstitialPage.js b/scripts/system/interstitialPage.js
index 8dd94623b7..8ecc982dab 100644
--- a/scripts/system/interstitialPage.js
+++ b/scripts/system/interstitialPage.js
@@ -325,6 +325,11 @@
                 leftMargin: domainNameLeftMargin
             };
 
+            // check to be sure we are going to look for an actual domain
+            if (!domain) {
+                doRequest = false;
+            }
+
             if (doRequest) {
                 var url = Account.metaverseServerURL + '/api/v1/places/' + domain;
                 request({
diff --git a/scripts/system/libraries/WebTablet.js b/scripts/system/libraries/WebTablet.js
index c0e3178521..9d333a1ae4 100644
--- a/scripts/system/libraries/WebTablet.js
+++ b/scripts/system/libraries/WebTablet.js
@@ -159,7 +159,7 @@ WebTablet = function (url, width, dpi, hand, location, visible) {
         url: url,
         localPosition: { x: 0, y: WEB_ENTITY_Y_OFFSET, z: -WEB_ENTITY_Z_OFFSET },
         localRotation: Quat.angleAxis(180, Y_AXIS),
-        dimensions: {x: screenWidth, y: screenHeight, z: 0.1},
+        dimensions: {x: screenWidth, y: screenHeight, z: 1.0},
         dpi: tabletDpi,
         color: { red: 255, green: 255, blue: 255 },
         alpha: 1.0,
@@ -176,7 +176,7 @@ WebTablet = function (url, width, dpi, hand, location, visible) {
     this.homeButtonID = Overlays.addOverlay("circle3d", {
         name: "homeButton",
         localPosition: { x: HOME_BUTTON_X_OFFSET, y: HOME_BUTTON_Y_OFFSET, z: -HOME_BUTTON_Z_OFFSET },
-        localRotation: { x: 0, y: 1, z: 0, w: 0},
+        localRotation: Quat.fromVec3Degrees({ x: 180, y: 180, z: 0}),
         dimensions: { x: homeButtonDim, y: homeButtonDim, z: homeButtonDim },
         solid: true,
         alpha: 0.0,
@@ -189,7 +189,7 @@ WebTablet = function (url, width, dpi, hand, location, visible) {
     this.homeButtonHighlightID = Overlays.addOverlay("circle3d", {
         name: "homeButtonHighlight",
         localPosition: { x: -HOME_BUTTON_X_OFFSET, y: HOME_BUTTON_Y_OFFSET, z: -HOME_BUTTON_Z_OFFSET },
-        localRotation: { x: 0, y: 1, z: 0, w: 0},
+        localRotation: Quat.fromVec3Degrees({ x: 180, y: 180, z: 0}),
         dimensions: { x: homeButtonDim, y: homeButtonDim, z: homeButtonDim },
         color: {red: 255, green: 255, blue: 255},
         solid: true,
diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js
index 767128ca16..269283ea6d 100644
--- a/scripts/system/libraries/entitySelectionTool.js
+++ b/scripts/system/libraries/entitySelectionTool.js
@@ -708,7 +708,7 @@ SelectionDisplay = (function() {
         shape: "Cone",
         solid: true,
         visible: false,
-        ignoreRayIntersection: false,
+        ignorePickIntersection: true,
         drawInFront: true
     };
     var handlePropertiesTranslateArrowCylinders = {
@@ -716,7 +716,7 @@ SelectionDisplay = (function() {
         shape: "Cylinder",
         solid: true,
         visible: false,
-        ignoreRayIntersection: false,
+        ignorePickIntersection: true,
         drawInFront: true
     };
     var handleTranslateXCone = Overlays.addOverlay("shape", handlePropertiesTranslateArrowCones);
@@ -741,7 +741,7 @@ SelectionDisplay = (function() {
         majorTickMarksAngle: ROTATE_DEFAULT_TICK_MARKS_ANGLE,
         majorTickMarksLength: 0.1,
         visible: false,
-        ignoreRayIntersection: false,
+        ignorePickIntersection: true,
         drawInFront: true
     };
     var handleRotatePitchRing = Overlays.addOverlay("circle3d", handlePropertiesRotateRings);
@@ -766,7 +766,7 @@ SelectionDisplay = (function() {
         solid: true,
         innerRadius: 0.9,
         visible: false,
-        ignoreRayIntersection: true,
+        ignorePickIntersection: true,
         drawInFront: true
     });
 
@@ -779,7 +779,7 @@ SelectionDisplay = (function() {
         visible: false,
         isFacingAvatar: true,
         drawInFront: true,
-        ignoreRayIntersection: true,
+        ignorePickIntersection: true,
         dimensions: { x: 0, y: 0 },
         lineHeight: 0.0,
         topMargin: 0,
@@ -791,7 +791,7 @@ SelectionDisplay = (function() {
     var handlePropertiesStretchCubes = {
         solid: true,
         visible: false,
-        ignoreRayIntersection: false,
+        ignorePickIntersection: true,
         drawInFront: true
     };
     var handleStretchXCube = Overlays.addOverlay("cube", handlePropertiesStretchCubes);
@@ -802,18 +802,17 @@ SelectionDisplay = (function() {
     Overlays.editOverlay(handleStretchZCube, { color: COLOR_BLUE });
 
     var handlePropertiesStretchPanel = {
-        shape: "Quad",
         alpha: 0.5,
         solid: true,
         visible: false,
-        ignoreRayIntersection: true,
+        ignorePickIntersection: true,
         drawInFront: true
     };
-    var handleStretchXPanel = Overlays.addOverlay("shape", handlePropertiesStretchPanel);
+    var handleStretchXPanel = Overlays.addOverlay("cube", handlePropertiesStretchPanel);
     Overlays.editOverlay(handleStretchXPanel, { color: COLOR_RED });
-    var handleStretchYPanel = Overlays.addOverlay("shape", handlePropertiesStretchPanel);
+    var handleStretchYPanel = Overlays.addOverlay("cube", handlePropertiesStretchPanel);
     Overlays.editOverlay(handleStretchYPanel, { color: COLOR_GREEN });
-    var handleStretchZPanel = Overlays.addOverlay("shape", handlePropertiesStretchPanel);
+    var handleStretchZPanel = Overlays.addOverlay("cube", handlePropertiesStretchPanel);
     Overlays.editOverlay(handleStretchZPanel, { color: COLOR_BLUE });
 
     var handleScaleCube = Overlays.addOverlay("cube", {
@@ -821,7 +820,7 @@ SelectionDisplay = (function() {
         color: COLOR_SCALE_CUBE,
         solid: true,
         visible: false,
-        ignoreRayIntersection: false,
+        ignorePickIntersection: true,
         drawInFront: true,
         borderSize: 1.4
     });
@@ -841,7 +840,7 @@ SelectionDisplay = (function() {
         color: COLOR_GREEN,
         solid: true,
         visible: false,
-        ignoreRayIntersection: false,
+        ignorePickIntersection: true,
         drawInFront: true,
         borderSize: 1.4
     });
@@ -854,6 +853,7 @@ SelectionDisplay = (function() {
         alpha: 0,
         solid: false,
         visible: false,
+        ignorePickIntersection: true,
         dashed: false
     });
 
@@ -865,6 +865,7 @@ SelectionDisplay = (function() {
         alpha: 0,
         solid: false,
         visible: false,
+        ignorePickIntersection: true,
         dashed: false
     });
 
@@ -877,7 +878,7 @@ SelectionDisplay = (function() {
             green: 0,
             blue: 0
         },
-        ignoreRayIntersection: true // always ignore this
+        ignorePickIntersection: true // always ignore this
     });
     var yRailOverlay = Overlays.addOverlay("line3d", {
         visible: false,
@@ -888,7 +889,7 @@ SelectionDisplay = (function() {
             green: 255,
             blue: 0
         },
-        ignoreRayIntersection: true // always ignore this
+        ignorePickIntersection: true // always ignore this
     });
     var zRailOverlay = Overlays.addOverlay("line3d", {
         visible: false,
@@ -899,7 +900,7 @@ SelectionDisplay = (function() {
             green: 0,
             blue: 255
         },
-        ignoreRayIntersection: true // always ignore this
+        ignorePickIntersection: true // always ignore this
     });
 
     var allOverlays = [
@@ -972,7 +973,7 @@ SelectionDisplay = (function() {
         color: COLOR_DEBUG_PICK_PLANE,
         solid: true,
         visible: false,
-        ignoreRayIntersection: true,
+        ignorePickIntersection: true,
         drawInFront: false
     });
     var debugPickPlaneHits = [];
@@ -1802,6 +1803,7 @@ SelectionDisplay = (function() {
                                isActiveTool(handleRotateYawRing) || 
                                isActiveTool(handleRotateRollRing);
             selectionBoxGeometry.visible = !inModeRotate && !isCameraInsideBox;
+            selectionBoxGeometry.ignorePickIntersection = !selectionBoxGeometry.visible;
             Overlays.editOverlay(selectionBox, selectionBoxGeometry);
 
             // UPDATE ICON TRANSLATE HANDLE
@@ -1811,9 +1813,13 @@ SelectionDisplay = (function() {
                     rotation: rotation
                 };
                 iconSelectionBoxGeometry.visible = !inModeRotate && isCameraInsideBox;
+                iconSelectionBoxGeometry.ignorePickIntersection = !iconSelectionBoxGeometry.visible;
                 Overlays.editOverlay(iconSelectionBox, iconSelectionBoxGeometry);
             } else {
-                Overlays.editOverlay(iconSelectionBox, { visible: false });
+                Overlays.editOverlay(iconSelectionBox, {
+                    visible: false,
+                    ignorePickIntersection: true
+                });
             }
 
             // UPDATE DUPLICATOR (CURRENTLY HIDDEN FOR NOW)
@@ -1882,7 +1888,7 @@ SelectionDisplay = (function() {
     // FUNCTION: SET OVERLAYS VISIBLE
     that.setOverlaysVisible = function(isVisible) {
         for (var i = 0, length = allOverlays.length; i < length; i++) {
-            Overlays.editOverlay(allOverlays[i], { visible: isVisible });
+            Overlays.editOverlay(allOverlays[i], { visible: isVisible, ignorePickIntersection: !isVisible });
         }
     };
 
@@ -1894,18 +1900,18 @@ SelectionDisplay = (function() {
     };
 
     that.setHandleTranslateXVisible = function(isVisible) {
-        Overlays.editOverlay(handleTranslateXCone, { visible: isVisible });
-        Overlays.editOverlay(handleTranslateXCylinder, { visible: isVisible });
+        Overlays.editOverlay(handleTranslateXCone, { visible: isVisible, ignorePickIntersection: !isVisible });
+        Overlays.editOverlay(handleTranslateXCylinder, { visible: isVisible, ignorePickIntersection: !isVisible });
     };
 
     that.setHandleTranslateYVisible = function(isVisible) {
-        Overlays.editOverlay(handleTranslateYCone, { visible: isVisible });
-        Overlays.editOverlay(handleTranslateYCylinder, { visible: isVisible });
+        Overlays.editOverlay(handleTranslateYCone, { visible: isVisible, ignorePickIntersection: !isVisible });
+        Overlays.editOverlay(handleTranslateYCylinder, { visible: isVisible, ignorePickIntersection: !isVisible });
     };
 
     that.setHandleTranslateZVisible = function(isVisible) {
-        Overlays.editOverlay(handleTranslateZCone, { visible: isVisible });
-        Overlays.editOverlay(handleTranslateZCylinder, { visible: isVisible });
+        Overlays.editOverlay(handleTranslateZCone, { visible: isVisible, ignorePickIntersection: !isVisible });
+        Overlays.editOverlay(handleTranslateZCylinder, { visible: isVisible, ignorePickIntersection: !isVisible });
     };
 
     // FUNCTION: SET HANDLE ROTATE VISIBLE
@@ -1916,15 +1922,15 @@ SelectionDisplay = (function() {
     };
 
     that.setHandleRotatePitchVisible = function(isVisible) {
-        Overlays.editOverlay(handleRotatePitchRing, { visible: isVisible });
+        Overlays.editOverlay(handleRotatePitchRing, { visible: isVisible, ignorePickIntersection: !isVisible });
     };
 
     that.setHandleRotateYawVisible = function(isVisible) {
-        Overlays.editOverlay(handleRotateYawRing, { visible: isVisible });
+        Overlays.editOverlay(handleRotateYawRing, { visible: isVisible, ignorePickIntersection: !isVisible });
     };
 
     that.setHandleRotateRollVisible = function(isVisible) {
-        Overlays.editOverlay(handleRotateRollRing, { visible: isVisible });
+        Overlays.editOverlay(handleRotateRollRing, { visible: isVisible, ignorePickIntersection: !isVisible });
     };
 
     // FUNCTION: SET HANDLE STRETCH VISIBLE
@@ -1935,15 +1941,15 @@ SelectionDisplay = (function() {
     };
 
     that.setHandleStretchXVisible = function(isVisible) {
-        Overlays.editOverlay(handleStretchXCube, { visible: isVisible });
+        Overlays.editOverlay(handleStretchXCube, { visible: isVisible, ignorePickIntersection: !isVisible });
     };
 
     that.setHandleStretchYVisible = function(isVisible) {
-        Overlays.editOverlay(handleStretchYCube, { visible: isVisible });
+        Overlays.editOverlay(handleStretchYCube, { visible: isVisible, ignorePickIntersection: !isVisible });
     };
 
     that.setHandleStretchZVisible = function(isVisible) {
-        Overlays.editOverlay(handleStretchZCube, { visible: isVisible });
+        Overlays.editOverlay(handleStretchZCube, { visible: isVisible, ignorePickIntersection: !isVisible });
     };
     
     // FUNCTION: SET HANDLE SCALE VISIBLE
@@ -1953,16 +1959,16 @@ SelectionDisplay = (function() {
     };
 
     that.setHandleScaleVisible = function(isVisible) {
-        Overlays.editOverlay(handleScaleCube, { visible: isVisible });
+        Overlays.editOverlay(handleScaleCube, { visible: isVisible, ignorePickIntersection: !isVisible });
     };
 
     that.setHandleBoundingBoxVisible = function(isVisible) {
-        Overlays.editOverlay(handleBoundingBox, { visible: isVisible });
+        Overlays.editOverlay(handleBoundingBox, { visible: isVisible, ignorePickIntersection: true });
     };
 
     // FUNCTION: SET HANDLE DUPLICATOR VISIBLE
     that.setHandleDuplicatorVisible = function(isVisible) {
-        Overlays.editOverlay(handleDuplicator, { visible: isVisible });
+        Overlays.editOverlay(handleDuplicator, { visible: isVisible, ignorePickIntersection: !isVisible });
     };
 
     // FUNCTION: DEBUG PICK PLANE
@@ -1975,7 +1981,7 @@ SelectionDisplay = (function() {
             position: pickPlanePosition,
             rotation: rotation,
             dimensions: dimensions,
-            visible: true 
+            visible: true
         });
     };
     
@@ -1986,7 +1992,7 @@ SelectionDisplay = (function() {
             shape: "Sphere",
             solid: true,
             visible: true,
-            ignoreRayIntersection: true,
+            ignorePickIntersection: true,
             drawInFront: false,
             color: COLOR_DEBUG_PICK_PLANE_HIT,
             position: pickHitPosition,
@@ -2082,10 +2088,12 @@ SelectionDisplay = (function() {
                 pushCommandForSelections(duplicatedEntityIDs);
                 if (isConstrained) {
                     Overlays.editOverlay(xRailOverlay, {
-                        visible: false
+                        visible: false,
+                        ignorePickIntersection: true
                     });
                     Overlays.editOverlay(zRailOverlay, {
-                        visible: false
+                        visible: false,
+                        ignorePickIntersection: true
                     });
                 }
             },
@@ -2174,22 +2182,26 @@ SelectionDisplay = (function() {
                         Overlays.editOverlay(xRailOverlay, {
                             start: xStart,
                             end: xEnd,
-                            visible: true
+                            visible: true,
+                            ignorePickIntersection: true
                         });
                         Overlays.editOverlay(zRailOverlay, {
                             start: zStart,
                             end: zEnd,
-                            visible: true
+                            visible: true,
+                            ignorePickIntersection: true
                         });
                         isConstrained = true;
                     }
                 } else {
                     if (isConstrained) {
                         Overlays.editOverlay(xRailOverlay, {
-                            visible: false
+                            visible: false,
+                            ignorePickIntersection: true
                         });
                         Overlays.editOverlay(zRailOverlay, {
-                            visible: false
+                            visible: false,
+                            ignorePickIntersection: true
                         });
                         isConstrained = false;
                     }
@@ -2460,7 +2472,7 @@ SelectionDisplay = (function() {
                 }
 
                 if (stretchPanel !== null) {
-                    Overlays.editOverlay(stretchPanel, { visible: true });
+                    Overlays.editOverlay(stretchPanel, { visible: true, ignorePickIntersection: false });
                 }
                 var stretchCubePosition = Overlays.getProperty(handleStretchCube, "position");
                 var stretchPanelPosition = Overlays.getProperty(stretchPanel, "position");
@@ -2481,7 +2493,7 @@ SelectionDisplay = (function() {
                 }
                 
                 if (stretchPanel !== null) {
-                    Overlays.editOverlay(stretchPanel, { visible: false });
+                    Overlays.editOverlay(stretchPanel, { visible: false, ignorePickIntersection: true });
                 }
                 activeStretchCubePanelOffset = null;
                 
@@ -2775,7 +2787,8 @@ SelectionDisplay = (function() {
                     rotation: worldRotation,
                     startAt: 0,
                     endAt: 0,
-                    visible: true
+                    visible: true,
+                    ignorePickIntersection: false
                 });
 
                 // editOverlays may not have committed rotation changes.
@@ -2805,13 +2818,13 @@ SelectionDisplay = (function() {
                 if (wantDebug) {
                     print("================== " + getMode() + "(addHandleRotateTool onEnd) -> =======================");
                 }
-                Overlays.editOverlay(rotationDegreesDisplay, { visible: false });
+                Overlays.editOverlay(rotationDegreesDisplay, { visible: false, ignorePickIntersection: true });
                 Overlays.editOverlay(selectedHandle, { 
                     hasTickMarks: false,
                     solid: true,
                     innerRadius: ROTATE_RING_IDLE_INNER_RADIUS
                 });
-                Overlays.editOverlay(handleRotateCurrentRing, { visible: false });
+                Overlays.editOverlay(handleRotateCurrentRing, { visible: false, ignorePickIntersection: true });
                 pushCommandForSelections();
                 if (wantDebug) {
                     print("================== " + getMode() + "(addHandleRotateTool onEnd) <- =======================");
diff --git a/scripts/system/libraries/utils.js b/scripts/system/libraries/utils.js
index 931c346299..508e8d46e3 100644
--- a/scripts/system/libraries/utils.js
+++ b/scripts/system/libraries/utils.js
@@ -395,7 +395,7 @@ resizeTablet = function (width, newParentJointIndex, sensorToWorldScaleOverride)
     var tabletDpi = DEFAULT_DPI * (DEFAULT_WIDTH / tabletWidth);
 
     // update tablet model dimensions
-    Overlays.editOverlay(HMD.tabletID, {
+    Entities.editEntity(HMD.tabletID, {
         dimensions: { x: tabletWidth, y: tabletHeight, z: tabletDepth }
     });
 
@@ -405,9 +405,9 @@ resizeTablet = function (width, newParentJointIndex, sensorToWorldScaleOverride)
     var screenWidth = 0.9367 * tabletWidth;
     var screenHeight = 0.9000 * tabletHeight;
     var landscape = Tablet.getTablet("com.highfidelity.interface.tablet.system").landscape;
-    Overlays.editOverlay(HMD.tabletScreenID, {
+    Entities.editEntity(HMD.tabletScreenID, {
         localPosition: { x: 0, y: WEB_ENTITY_Y_OFFSET, z: -WEB_ENTITY_Z_OFFSET},
-        dimensions: {x: landscape ? screenHeight : screenWidth, y: landscape ? screenWidth : screenHeight, z: 0.1},
+        dimensions: {x: landscape ? screenHeight : screenWidth, y: landscape ? screenWidth : screenHeight, z: 1.0},
         dpi: tabletDpi
     });
 
@@ -416,19 +416,79 @@ resizeTablet = function (width, newParentJointIndex, sensorToWorldScaleOverride)
     var HOME_BUTTON_X_OFFSET = 0.00079 * sensorScaleOffsetOverride * sensorScaleFactor;
     var HOME_BUTTON_Y_OFFSET = -1 * ((tabletHeight / 2) - (4.0 * tabletScaleFactor / 2)) * sensorScaleOffsetOverride;
     var HOME_BUTTON_Z_OFFSET = (tabletDepth / 1.9) * sensorScaleOffsetOverride;
-    Overlays.editOverlay(HMD.homeButtonID, {
+    Entities.editEntity(HMD.homeButtonID, {
         localPosition: { x: HOME_BUTTON_X_OFFSET, y: HOME_BUTTON_Y_OFFSET, z: -HOME_BUTTON_Z_OFFSET },
-        localRotation: { x: 0, y: 1, z: 0, w: 0 },
         dimensions: { x: homeButtonDim, y: homeButtonDim, z: homeButtonDim }
     });
 
-    Overlays.editOverlay(HMD.homeButtonHighlightID, {
+    Entities.editEntity(HMD.homeButtonHighlightID, {
         localPosition: { x: -HOME_BUTTON_X_OFFSET, y: HOME_BUTTON_Y_OFFSET, z: -HOME_BUTTON_Z_OFFSET },
-        localRotation: { x: 0, y: 1, z: 0, w: 0 },
         dimensions: { x: homeButtonDim, y: homeButtonDim, z: homeButtonDim }
     });
 };
 
+
+reparentAndScaleTablet = function(width, reparentProps) {
+
+    if (!HMD.tabletID || !HMD.tabletScreenID || !HMD.homeButtonID || !HMD.homeButtonHighlightID) {
+        return;
+    }
+    var sensorScaleFactor = MyAvatar.sensorToWorldScale;
+    var sensorScaleOffsetOverride = 1;
+    var SENSOR_TO_ROOM_MATRIX = 65534;
+    var parentJointIndex = reparentProps.parentJointIndex;
+    if (parentJointIndex === SENSOR_TO_ROOM_MATRIX) {
+        sensorScaleOffsetOverride = 1 / sensorScaleFactor;
+    }
+
+
+    // will need to be recaclulated if dimensions of fbx model change.
+    var TABLET_NATURAL_DIMENSIONS = {x: 32.083, y: 48.553, z: 2.269};
+    var DEFAULT_DPI = 31;
+    var DEFAULT_WIDTH = 0.4375;
+
+    // scale factor of natural tablet dimensions.
+    var tabletWidth = (width || DEFAULT_WIDTH) * sensorScaleFactor;
+    var tabletScaleFactor = tabletWidth / TABLET_NATURAL_DIMENSIONS.x;
+    var tabletHeight = TABLET_NATURAL_DIMENSIONS.y * tabletScaleFactor;
+    var tabletDepth = TABLET_NATURAL_DIMENSIONS.z * tabletScaleFactor;
+    var tabletDpi = DEFAULT_DPI * (DEFAULT_WIDTH / tabletWidth);
+
+    // update tablet model dimensions
+
+    Entities.editEntity(HMD.tabletID, {
+        parentID: reparentProps.parentID,
+        parentJointIndex: reparentProps.parentJointIndex,
+        dimensions: { x: tabletWidth, y: tabletHeight, z: tabletDepth}
+    });
+    // update webOverlay
+    var WEB_ENTITY_Z_OFFSET = (tabletDepth / 2.5) * sensorScaleOffsetOverride;
+    var WEB_ENTITY_Y_OFFSET = 1.25 * tabletScaleFactor * sensorScaleOffsetOverride;
+    var screenWidth = 0.9367 * tabletWidth;
+    var screenHeight = 0.9000 * tabletHeight;
+    var landscape = Tablet.getTablet("com.highfidelity.interface.tablet.system").landscape;
+    Entities.editEntity(HMD.tabletScreenID, {
+        localPosition: { x: 0, y: WEB_ENTITY_Y_OFFSET, z: -WEB_ENTITY_Z_OFFSET},
+        dimensions: {x: landscape ? screenHeight : screenWidth, y: landscape ? screenWidth : screenHeight, z: 1.0},
+        dpi: tabletDpi
+    });
+
+    // update homeButton
+    var homeButtonDim = 4.0 * tabletScaleFactor / 1.5;
+    var HOME_BUTTON_X_OFFSET = 0.00079 * sensorScaleOffsetOverride * sensorScaleFactor;
+    var HOME_BUTTON_Y_OFFSET = -1 * ((tabletHeight / 2) - (4.0 * tabletScaleFactor / 2)) * sensorScaleOffsetOverride;
+    var HOME_BUTTON_Z_OFFSET = (tabletDepth / 1.9) * sensorScaleOffsetOverride;
+    Entities.editEntity(HMD.homeButtonID, {
+        localPosition: { x: HOME_BUTTON_X_OFFSET, y: HOME_BUTTON_Y_OFFSET, z: -HOME_BUTTON_Z_OFFSET },
+        dimensions: { x: homeButtonDim, y: homeButtonDim, z: homeButtonDim }
+    });
+
+    Entities.editEntity(HMD.homeButtonHighlightID, {
+        localPosition: { x: -HOME_BUTTON_X_OFFSET, y: HOME_BUTTON_Y_OFFSET, z: -HOME_BUTTON_Z_OFFSET },
+        dimensions: { x: homeButtonDim, y: homeButtonDim, z: homeButtonDim }
+    });
+}
+
 getMainTabletIDs = function () {
     var tabletIDs = [];
     if (HMD.tabletID) {
diff --git a/scripts/system/modules/createWindow.js b/scripts/system/modules/createWindow.js
index 0c4412abfb..7369cf91f8 100644
--- a/scripts/system/modules/createWindow.js
+++ b/scripts/system/modules/createWindow.js
@@ -125,9 +125,6 @@ module.exports = (function() {
 
             Script.scriptEnding.connect(this, function() {
                 this.window.close();
-                // FIXME: temp solution for reload crash (MS18269),
-                // we should decide on proper object ownership strategy for InteractiveWindow API
-                this.window = null;
             });
         },
         setVisible: function(visible) {
diff --git a/scripts/system/pal.js b/scripts/system/pal.js
index 141ea03330..0c4338b31d 100644
--- a/scripts/system/pal.js
+++ b/scripts/system/pal.js
@@ -161,7 +161,8 @@ ExtendedOverlay.unHover = function () { // calls hover(false) on lastHoveringId
 // hit(overlay) on the one overlay intersected by pickRay, if any.
 // noHit() if no ExtendedOverlay was intersected (helps with hover)
 ExtendedOverlay.applyPickRay = function (pickRay, hit, noHit) {
-    var pickedOverlay = Overlays.findRayIntersection(pickRay); // Depends on nearer coverOverlays to extend closer to us than farther ones.
+    // TODO: this could just include the necessary overlays for better performance
+    var pickedOverlay = Overlays.findRayIntersection(pickRay, true); // Depends on nearer coverOverlays to extend closer to us than farther ones.
     if (!pickedOverlay.intersects) {
         if (noHit) {
             return noHit();
diff --git a/scripts/system/tablet-ui/tabletUI.js b/scripts/system/tablet-ui/tabletUI.js
index f9e9165f2e..60848224bb 100644
--- a/scripts/system/tablet-ui/tabletUI.js
+++ b/scripts/system/tablet-ui/tabletUI.js
@@ -137,11 +137,11 @@
                 UIWebTablet.calculateTabletAttachmentProperties(activeHand, true, tabletProperties);
             }
             tabletProperties.visible = true;
+            tabletProperties.ignorePickIntersection = false;
             Overlays.editOverlay(HMD.tabletID, tabletProperties);
-            Overlays.editOverlay(HMD.homeButtonID, { visible: true });
-            Overlays.editOverlay(HMD.homeButtonHighlightID, { visible: true });
-            Overlays.editOverlay(HMD.tabletScreenID, { visible: true });
-            Overlays.editOverlay(HMD.tabletScreenID, { maxFPS: 90 });
+            Overlays.editOverlay(HMD.homeButtonID, { visible: true, ignorePickIntersection: false });
+            Overlays.editOverlay(HMD.homeButtonHighlightID, { visible: true, ignorePickIntersection: false });
+            Overlays.editOverlay(HMD.tabletScreenID, { visible: true, ignorePickIntersection: false, maxFPS: 90 });
             updateTabletWidthFromSettings(true);
         }
         gTablet.tabletShown = true;
@@ -158,11 +158,10 @@
             print("TABLET hide");
         }
 
-        Overlays.editOverlay(HMD.tabletID, { visible: false });
-        Overlays.editOverlay(HMD.homeButtonID, { visible: false });
-        Overlays.editOverlay(HMD.homeButtonHighlightID, { visible: false });
-        Overlays.editOverlay(HMD.tabletScreenID, { visible: false });
-        Overlays.editOverlay(HMD.tabletScreenID, { maxFPS: 1 });
+        Overlays.editOverlay(HMD.tabletID, { visible: false, ignorePickIntersection: true });
+        Overlays.editOverlay(HMD.homeButtonID, { visible: false, ignorePickIntersection: true });
+        Overlays.editOverlay(HMD.homeButtonHighlightID, { visible: false, ignorePickIntersection: true });
+        Overlays.editOverlay(HMD.tabletScreenID, { visible: false, ignorePickIntersection: true, maxFPS: 1 });
     }
 
     function closeTabletUI() {
diff --git a/tests/animation/CMakeLists.txt b/tests/animation/CMakeLists.txt
index 2af4d5f2cd..e378750425 100644
--- a/tests/animation/CMakeLists.txt
+++ b/tests/animation/CMakeLists.txt
@@ -1,7 +1,7 @@
 # Declare dependencies
 macro (setup_testcase_dependencies)
   # link in the shared libraries
-  link_hifi_libraries(shared animation gpu fbx hfm graphics networking test-utils)
+  link_hifi_libraries(shared animation gpu fbx hfm graphics networking test-utils image)
 
   package_libraries_for_deployment()
 endmacro ()
diff --git a/tests/animation/src/AnimTests.cpp b/tests/animation/src/AnimTests.cpp
index 0cd9571e22..a14ffcf967 100644
--- a/tests/animation/src/AnimTests.cpp
+++ b/tests/animation/src/AnimTests.cpp
@@ -443,6 +443,28 @@ void AnimTests::testAnimPose() {
             }
         }
     }
+
+
+    // test matrix that has a negative determiant.
+    glm::vec4 col0(-9.91782e-05f, -5.40349e-05f, 0.000724383f, 0.0f);
+    glm::vec4 col1(-0.000155237f, 0.00071579f, 3.21398e-05f, 0.0f);
+    glm::vec4 col2(0.000709614f, 0.000149036f, 0.000108273f, 0.0f);
+    glm::vec4 col3(0.117922f, 0.250457f, 0.102155f, 1.0f);
+    glm::mat4 m(col0, col1, col2, col3);
+    AnimPose p(m);
+
+    glm::vec3 resultTrans = glm::vec3(col3);
+    glm::quat resultRot = glm::quat(0.0530394f, 0.751549f, 0.0949531f, -0.650649f);
+    glm::vec3 resultScale = glm::vec3(-0.000733135f, -0.000733135f, -0.000733135f);
+
+    const float TEST_EPSILON2 = 0.00001f;
+    QCOMPARE_WITH_ABS_ERROR(p.trans(), resultTrans, TEST_EPSILON2);
+
+    if (glm::dot(p.rot(), resultRot) < 0.0f) {
+        resultRot = -resultRot;
+    }
+    QCOMPARE_WITH_ABS_ERROR(p.rot(), resultRot, TEST_EPSILON2);
+    QCOMPARE_WITH_ABS_ERROR(p.scale(), resultScale, TEST_EPSILON2);
 }
 
 void AnimTests::testExpressionTokenizer() {
diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt
index 6cda67db2d..b9ae635a4f 100644
--- a/tools/CMakeLists.txt
+++ b/tools/CMakeLists.txt
@@ -20,7 +20,7 @@ endfunction()
 
 if (BUILD_TOOLS)
     # Allow different tools for stable builds
-    if (RELEASE_TYPE STREQUAL "PRODUCTION")
+    if (STABLE_BUILD)
         set(ALL_TOOLS 
             udt-test 
             vhacd-util
diff --git a/tools/jsdoc/.gitignore b/tools/jsdoc/.gitignore
index 148363ca03..b64f841525 100644
--- a/tools/jsdoc/.gitignore
+++ b/tools/jsdoc/.gitignore
@@ -1,2 +1,6 @@
-out
+out/*
+!out/README.md
+
+
+
 
diff --git a/tools/jsdoc/config.json b/tools/jsdoc/config.json
index a24e248661..5074362225 100644
--- a/tools/jsdoc/config.json
+++ b/tools/jsdoc/config.json
@@ -1,4 +1,17 @@
 {
+    "opts": {
+        "template": "hifi-jsdoc-template"
+    },
+    "docdash": {
+        "meta": {                       
+            "title": "",                
+            "description": "",          
+            "keyword": ""               
+        },
+        "search": [true],  
+        "collapse": [true],       
+        "typedefs": [false]    
+    },    
     "templates": {
         "default": {
             "outputSourceFiles": false
diff --git a/tools/jsdoc/hifi-jsdoc-template/LICENSE.md b/tools/jsdoc/hifi-jsdoc-template/LICENSE.md
new file mode 100644
index 0000000000..ff66af581b
--- /dev/null
+++ b/tools/jsdoc/hifi-jsdoc-template/LICENSE.md
@@ -0,0 +1,61 @@
+# License
+
+Docdash is free software, licensed under the Apache License, Version 2.0 (the
+"License"). Commercial and non-commercial use are permitted in compliance with
+the License.
+
+Copyright (c) 2016 Clement Moron  and the
+[contributors to docdash](https://github.com/clenemt/docdash/graphs/contributors).
+All rights reserved.
+
+You may obtain a copy of the License at:
+http://www.apache.org/licenses/LICENSE-2.0
+
+In addition, a copy of the License is included with this distribution.
+
+As stated in Section 7, "Disclaimer of Warranty," of the License:
+
+> Licensor provides the Work (and each Contributor provides its Contributions)
+> on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+> express or implied, including, without limitation, any warranties or
+> conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+> PARTICULAR PURPOSE. You are solely responsible for determining the
+> appropriateness of using or redistributing the Work and assume any risks
+> associated with Your exercise of permissions under this License.
+
+The source code for docdash is available at:
+https://github.com/clenemt/docdash
+
+# Third-Party Software
+
+Docdash includes or depends upon the following third-party software, either in
+whole or in part. Each third-party software package is provided under its own
+license.
+
+## JSDoc 3
+
+JSDoc 3 is free software, licensed under the Apache License, Version 2.0 (the
+"License"). Commercial and non-commercial use are permitted in compliance with
+the License.
+
+Copyright (c) 2011-2016 Michael Mathews  and the
+[contributors to JSDoc](https://github.com/jsdoc3/jsdoc/graphs/contributors).
+All rights reserved.
+
+You may obtain a copy of the License at:
+http://www.apache.org/licenses/LICENSE-2.0
+
+In addition, a copy of the License is included with this distribution.
+
+As stated in Section 7, "Disclaimer of Warranty," of the License:
+
+> Licensor provides the Work (and each Contributor provides its Contributions)
+> on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+> express or implied, including, without limitation, any warranties or
+> conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+> PARTICULAR PURPOSE. You are solely responsible for determining the
+> appropriateness of using or redistributing the Work and assume any risks
+> associated with Your exercise of permissions under this License.
+
+The source code for JSDoc 3 is available at:
+https://github.com/jsdoc3/jsdoc
diff --git a/tools/jsdoc/hifi-jsdoc-template/README.md b/tools/jsdoc/hifi-jsdoc-template/README.md
new file mode 100644
index 0000000000..797beaa79a
--- /dev/null
+++ b/tools/jsdoc/hifi-jsdoc-template/README.md
@@ -0,0 +1,42 @@
+# hifi-jsdoc-template
+The hifi-jsdoc-template is based on the [DocDash](https://github.com/clenemt/docdash) template. 
+
+## Usage
+Clone repository to your designated `jsdoc/node_modules` template directory. 
+
+In your `config.json` file, add a template option.
+
+```json
+"opts": {
+  "template": "node_modules/hifi-jsdoc-template"
+}
+```
+
+## Sample `config.json`
+
+```json
+{
+    "opts": {
+        "template": "node_modules/hifi-jsdoc-template"
+    },
+    "docdash": {
+        "meta": {                       
+            "title": "",                
+            "description": "",          
+            "keyword": ""               
+        },
+        "search": [true],  
+        "collapse": [true],       
+        "typedefs": [false]    
+    },    
+    "templates": {
+        "default": {
+            "outputSourceFiles": false
+        }
+    },
+    "plugins": [
+        "plugins/hifi",
+        "plugins/hifiJSONExport"
+    ]
+}
+```
diff --git a/tools/jsdoc/hifi-jsdoc-template/package.json b/tools/jsdoc/hifi-jsdoc-template/package.json
new file mode 100644
index 0000000000..f011adc2ba
--- /dev/null
+++ b/tools/jsdoc/hifi-jsdoc-template/package.json
@@ -0,0 +1,59 @@
+{
+  "_from": "docdash",
+  "_id": "docdash@1.0.0",
+  "_inBundle": false,
+  "_integrity": "sha512-HhK72PT4z55og8FDqskO/tTYXxU+LovRz+9pCDHLnUoPchkxjdIJidS+96LqW3CLrRdBmnkDRrcVrDFGLIluTw==",
+  "_location": "/docdash",
+  "_phantomChildren": {},
+  "_requested": {
+    "type": "tag",
+    "registry": true,
+    "raw": "docdash",
+    "name": "docdash",
+    "escapedName": "docdash",
+    "rawSpec": "",
+    "saveSpec": null,
+    "fetchSpec": "latest"
+  },
+  "_requiredBy": [
+    "#USER",
+    "/"
+  ],
+  "_resolved": "https://registry.npmjs.org/docdash/-/docdash-1.0.0.tgz",
+  "_shasum": "5b7df10fed3d341fc4416a8978c65ad561869d18",
+  "_spec": "docdash",
+  "_where": "D:\\hifi\\tools\\jsdoc",
+  "author": {
+    "name": "Clement Moron",
+    "email": "clement.moron@gmail.com"
+  },
+  "bugs": {
+    "url": "https://github.com/clenemt/docdash/issues"
+  },
+  "bundleDependencies": false,
+  "deprecated": false,
+  "description": "A clean, responsive documentation template theme for JSDoc 3 inspired by lodash and minami",
+  "devDependencies": {
+    "browser-sync": "latest",
+    "jsdoc": "latest",
+    "watch-run": "latest"
+  },
+  "homepage": "https://github.com/clenemt/docdash#readme",
+  "keywords": [
+    "jsdoc",
+    "template"
+  ],
+  "license": "Apache-2.0",
+  "main": "publish.js",
+  "name": "docdash",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/clenemt/docdash.git"
+  },
+  "scripts": {
+    "sync": "browser-sync start -s ../fixtures-doc -f ../fixtures-doc --reload-delay 1000 --no-ui --no-notify",
+    "test": "jsdoc -c fixtures/fixtures.conf.json",
+    "watch": "watch-run -d 1000 -p tmpl/**,static/** \"npm run test\""
+  },
+  "version": "1.0.0"
+}
diff --git a/tools/jsdoc/hifi-jsdoc-template/publish.js b/tools/jsdoc/hifi-jsdoc-template/publish.js
new file mode 100644
index 0000000000..9cd428bbbb
--- /dev/null
+++ b/tools/jsdoc/hifi-jsdoc-template/publish.js
@@ -0,0 +1,772 @@
+/*global env: true */
+'use strict';
+
+var doop = require('jsdoc/util/doop');
+var fs = require('jsdoc/fs');
+var helper = require('jsdoc/util/templateHelper');
+var logger = require('jsdoc/util/logger');
+var path = require('jsdoc/path');
+var taffy = require('taffydb').taffy;
+var template = require('jsdoc/template');
+var util = require('util');
+
+var htmlsafe = helper.htmlsafe;
+var linkto = helper.linkto;
+var resolveAuthorLinks = helper.resolveAuthorLinks;
+var scopeToPunc = helper.scopeToPunc;
+var hasOwnProp = Object.prototype.hasOwnProperty;
+
+var data;
+var view;
+
+var outdir = path.normalize(env.opts.destination);
+
+function copyFile(source, target, cb) {
+  var cbCalled = false;
+
+  var rd = fs.createReadStream(source);
+  rd.on("error", function(err) {
+    done(err);
+  });
+  var wr = fs.createWriteStream(target);
+  wr.on("error", function(err) {
+    done(err);
+  });
+  wr.on("close", function(ex) {
+    done();
+  });
+  rd.pipe(wr);
+
+  function done(err) {
+    if (!cbCalled) {
+      cb(err);
+      cbCalled = true;
+    }
+  }
+}
+
+function find(spec) {
+    return helper.find(data, spec);
+}
+
+function tutoriallink(tutorial) {
+    return helper.toTutorial(tutorial, null, { tag: 'em', classname: 'disabled', prefix: 'Tutorial: ' });
+}
+
+function getAncestorLinks(doclet) {
+    return helper.getAncestorLinks(data, doclet);
+}
+
+function hashToLink(doclet, hash) {
+    if ( !/^(#.+)/.test(hash) ) { return hash; }
+
+    var url = helper.createLink(doclet);
+
+    url = url.replace(/(#.+|$)/, hash);
+    return '' + hash + '';
+}
+
+function needsSignature(doclet) {
+    var needsSig = false;
+
+    // function and class definitions always get a signature
+    if (doclet.kind === 'function' || doclet.kind === 'class' && !doclet.hideconstructor) {
+        needsSig = true;
+    }
+    // typedefs that contain functions get a signature, too
+    else if (doclet.kind === 'typedef' && doclet.type && doclet.type.names &&
+        doclet.type.names.length) {
+        for (var i = 0, l = doclet.type.names.length; i < l; i++) {
+            if (doclet.type.names[i].toLowerCase() === 'function') {
+                needsSig = true;
+                break;
+            }
+        }
+    }
+
+    return needsSig;
+}
+
+function getSignatureAttributes(item) {
+    var attributes = [];
+
+    if (item.optional) {
+        attributes.push('opt');
+    }
+
+    if (item.nullable === true) {
+        attributes.push('nullable');
+    }
+    else if (item.nullable === false) {
+        attributes.push('non-null');
+    }
+
+    return attributes;
+}
+
+function updateItemName(item) {
+    var attributes = getSignatureAttributes(item);
+    var itemName = item.name || '';
+
+    if (item.variable) {
+        itemName = '…' + itemName;
+    }
+
+    if (attributes && attributes.length) {
+        itemName = util.format( '%s%s', itemName,
+            attributes.join(', ') );
+    }
+
+    return itemName;
+}
+
+function addParamAttributes(params) {
+    return params.filter(function(param) {
+        return param.name && param.name.indexOf('.') === -1;
+    }).map(updateItemName);
+}
+
+function buildItemTypeStrings(item) {
+    var types = [];
+
+    if (item && item.type && item.type.names) {
+        item.type.names.forEach(function(name) {
+            types.push( linkto(name, htmlsafe(name)) );
+        });
+    }
+
+    return types;
+}
+
+function buildAttribsString(attribs) {
+    var attribsString = '';
+
+    if (attribs && attribs.length) {
+        attribsString = htmlsafe( util.format('(%s) ', attribs.join(', ')) );
+    }
+
+    return attribsString;
+}
+
+function addNonParamAttributes(items) {
+    var types = [];
+
+    items.forEach(function(item) {
+        types = types.concat( buildItemTypeStrings(item) );
+    });
+
+    return types;
+}
+
+function addSignatureParams(f) {
+    var params = f.params ? addParamAttributes(f.params) : [];
+    f.signature = util.format( '%s( %s )', (f.signature || ''), params.join(', ') );
+}
+
+function addSignatureReturns(f) {
+    var attribs = [];
+    var attribsString = '';
+    var returnTypes = [];
+    var returnTypesString = '';
+
+    // jam all the return-type attributes into an array. this could create odd results (for example,
+    // if there are both nullable and non-nullable return types), but let's assume that most people
+    // who use multiple @return tags aren't using Closure Compiler type annotations, and vice-versa.
+    if (f.returns) {
+        f.returns.forEach(function(item) {
+            helper.getAttribs(item).forEach(function(attrib) {
+                if (attribs.indexOf(attrib) === -1) {
+                    attribs.push(attrib);
+                }
+            });
+        });
+
+        attribsString = buildAttribsString(attribs);
+    }
+
+    if (f.returns) {
+        returnTypes = addNonParamAttributes(f.returns);
+    }
+    if (returnTypes.length) {
+        returnTypesString = util.format( ' → %s{%s}', attribsString, returnTypes.join('|') );
+    }
+
+    f.signature = '' + (f.signature || '') + '' +
+        '' + returnTypesString + '';
+}
+
+function addSignatureTypes(f) {
+    var types = f.type ? buildItemTypeStrings(f) : [];
+
+    f.signature = (f.signature || '') + '' +
+        (types.length ? ' :' + types.join('|') : '') + '';
+}
+
+function addAttribs(f) {
+    var attribs = helper.getAttribs(f);
+    var attribsString = buildAttribsString(attribs);
+
+    f.attribs = util.format('%s', attribsString);
+}
+
+function shortenPaths(files, commonPrefix) {
+    Object.keys(files).forEach(function(file) {
+        files[file].shortened = files[file].resolved.replace(commonPrefix, '')
+            // always use forward slashes
+            .replace(/\\/g, '/');
+    });
+
+    return files;
+}
+
+function getPathFromDoclet(doclet) {
+    if (!doclet.meta) {
+        return null;
+    }
+
+    return doclet.meta.path && doclet.meta.path !== 'null' ?
+        path.join(doclet.meta.path, doclet.meta.filename) :
+        doclet.meta.filename;
+}
+
+function generate(type, title, docs, filename, resolveLinks) {
+    resolveLinks = resolveLinks === false ? false : true;
+
+    var docData = {
+        type: type,
+        title: title,
+        docs: docs
+    };
+
+    var outpath = path.join(outdir, filename),
+        html = view.render('container.tmpl', docData);
+
+    if (resolveLinks) {
+        html = helper.resolveLinks(html); // turn {@link foo} into foo
+    }
+
+    fs.writeFileSync(outpath, html, 'utf8');
+}
+
+function generateSourceFiles(sourceFiles, encoding) {
+    encoding = encoding || 'utf8';
+    Object.keys(sourceFiles).forEach(function(file) {
+        var source;
+        // links are keyed to the shortened path in each doclet's `meta.shortpath` property
+        var sourceOutfile = helper.getUniqueFilename(sourceFiles[file].shortened);
+        helper.registerLink(sourceFiles[file].shortened, sourceOutfile);
+
+        try {
+            source = {
+                kind: 'source',
+                code: helper.htmlsafe( fs.readFileSync(sourceFiles[file].resolved, encoding) )
+            };
+        }
+        catch(e) {
+            logger.error('Error while generating source file %s: %s', file, e.message);
+        }
+
+        generate('Source', sourceFiles[file].shortened, [source], sourceOutfile, false);
+    });
+}
+
+/**
+ * Look for classes or functions with the same name as modules (which indicates that the module
+ * exports only that class or function), then attach the classes or functions to the `module`
+ * property of the appropriate module doclets. The name of each class or function is also updated
+ * for display purposes. This function mutates the original arrays.
+ *
+ * @private
+ * @param {Array.} doclets - The array of classes and functions to
+ * check.
+ * @param {Array.} modules - The array of module doclets to search.
+ */
+function attachModuleSymbols(doclets, modules) {
+    var symbols = {};
+
+    // build a lookup table
+    doclets.forEach(function(symbol) {
+        symbols[symbol.longname] = symbols[symbol.longname] || [];
+        symbols[symbol.longname].push(symbol);
+    });
+
+    return modules.map(function(module) {
+        if (symbols[module.longname]) {
+            module.modules = symbols[module.longname]
+                // Only show symbols that have a description. Make an exception for classes, because
+                // we want to show the constructor-signature heading no matter what.
+                .filter(function(symbol) {
+                    return symbol.description || symbol.kind === 'class';
+                })
+                .map(function(symbol) {
+                    symbol = doop(symbol);
+
+                    if (symbol.kind === 'class' || symbol.kind === 'function' && !symbol.hideconstructor) {
+                        symbol.name = symbol.name.replace('module:', '(require("') + '"))';
+                    }
+
+                    return symbol;
+                });
+        }
+    });
+}
+
+function buildMemberNav(items, itemHeading, itemsSeen, linktoFn) {
+    var nav = '';
+
+    if (items && items.length) {
+        var itemsNav = '';
+
+        items.forEach(function(item) {
+            var displayName;
+            var methods = find({kind:'function', memberof: item.longname});
+            var signals = find({kind:'signal', memberof: item.longname});
+            var members = find({kind:'member', memberof: item.longname});
+            var docdash = env && env.conf && env.conf.docdash || {};
+            var conf = env && env.conf || {};
+            if ( !hasOwnProp.call(item, 'longname') ) {
+                itemsNav += '
  • ' + linktoFn('', item.name); + itemsNav += '
  • '; + } else if ( !hasOwnProp.call(itemsSeen, item.longname) ) { + if (conf.templates.default.useLongnameInNav) { + displayName = item.longname; + } else { + displayName = item.name; + } + itemsNav += '
  • ' + linktoFn(item.longname, displayName.replace(/\b(module|event):/g, '')); + + if (docdash.static && members.find(function (m) { return m.scope === 'static'; } )) { + itemsNav += "
      "; + + members.forEach(function (member) { + if (!member.scope === 'static') return; + itemsNav += "
    • '; + } + } + + return nav; +} + +function linktoTutorial(longName, name) { + return tutoriallink(name); +} + +function linktoExternal(longName, name) { + return linkto(longName, name.replace(/(^"|"$)/g, '')); +} + +/** + * Create the navigation sidebar. + * @param {object} members The members that will be used to create the sidebar. + * @param {array} members.classes + * @param {array} members.externals + * @param {array} members.globals + * @param {array} members.mixins + * @param {array} members.modules + * @param {array} members.namespaces + * @param {array} members.tutorials + * @param {array} members.events + * @param {array} members.interfaces + * @return {s + ring} The HTML for the navigation sidebar. + */ + +function buildNav(members) { + var nav = '

      Home

      '; + var seen = {}; + var seenTutorials = {}; + var docdash = env && env.conf && env.conf.docdash || {}; + if(docdash.menu){ + for(var menu in docdash.menu){ + nav += '

      '; + } + } + var defaultOrder = [ + 'Namespaces', 'Classes', 'Modules', 'Externals', 'Events', 'Mixins', 'Tutorials', 'Interfaces' + ]; + var order = docdash.sectionOrder || defaultOrder; + var sections = { + Namespaces: buildMemberNav(members.namespaces, 'Namespaces', seen, linkto), + Classes: buildMemberNav(members.classes, 'Classes', seen, linkto), + Modules: buildMemberNav(members.modules, 'Modules', {}, linkto), + Externals: buildMemberNav(members.externals, 'Externals', seen, linktoExternal), + Events: buildMemberNav(members.events, 'Events', seen, linkto), + Mixins: buildMemberNav(members.mixins, 'Mixins', seen, linkto), + Tutorials: buildMemberNav(members.tutorials, 'Tutorials', seenTutorials, linktoTutorial), + Interfaces: buildMemberNav(members.interfaces, 'Interfaces', seen, linkto), + }; + order.forEach(member => nav += sections[member]); + + if (members.globals.length) { + var globalNav = ''; + + members.globals.forEach(function(g) { + if ( (docdash.typedefs || g.kind !== 'typedef') && !hasOwnProp.call(seen, g.longname) ) { + globalNav += '
    • ' + linkto(g.longname, g.name) + '
    • '; + } + seen[g.longname] = true; + }); + + if (!globalNav) { + // turn the heading into a link so you can actually get to the global page + nav += '

      ' + linkto('global', 'Global') + '

      '; + } + else { + nav += '

      Globals

        ' + globalNav + '
      '; + } + } + + return nav; +} + +/** + @param {TAFFY} taffyData See . + @param {object} opts + @param {Tutorial} tutorials + */ +exports.publish = function(taffyData, opts, tutorials) { + var docdash = env && env.conf && env.conf.docdash || {}; + data = taffyData; + + var conf = env.conf.templates || {}; + conf.default = conf.default || {}; + + var templatePath = path.normalize(opts.template); + view = new template.Template( path.join(templatePath, 'tmpl') ); + + // claim some special filenames in advance, so the All-Powerful Overseer of Filename Uniqueness + // doesn't try to hand them out later + var indexUrl = helper.getUniqueFilename('index'); + // don't call registerLink() on this one! 'index' is also a valid longname + + var globalUrl = helper.getUniqueFilename('global'); + helper.registerLink('global', globalUrl); + + // set up templating + view.layout = conf.default.layoutFile ? + path.getResourcePath(path.dirname(conf.default.layoutFile), + path.basename(conf.default.layoutFile) ) : + 'layout.tmpl'; + + // set up tutorials for helper + helper.setTutorials(tutorials); + + data = helper.prune(data); + + docdash.sort !== false && data.sort('longname, version, since'); + helper.addEventListeners(data); + + var sourceFiles = {}; + var sourceFilePaths = []; + data().each(function(doclet) { + if(docdash.removeQuotes){ + if(docdash.removeQuotes === "all"){ + if(doclet.name){ + doclet.name = doclet.name.replace(/"/g, ''); + doclet.name = doclet.name.replace(/'/g, ''); + } + if(doclet.longname){ + doclet.longname = doclet.longname.replace(/"/g, ''); + doclet.longname = doclet.longname.replace(/'/g, ''); + } + } + else if(docdash.removeQuotes === "trim"){ + if(doclet.name){ + doclet.name = doclet.name.replace(/^"(.*)"$/, '$1'); + doclet.name = doclet.name.replace(/^'(.*)'$/, '$1'); + } + if(doclet.longname){ + doclet.longname = doclet.longname.replace(/^"(.*)"$/, '$1'); + doclet.longname = doclet.longname.replace(/^'(.*)'$/, '$1'); + } + } + } + doclet.attribs = ''; + + if (doclet.examples) { + doclet.examples = doclet.examples.map(function(example) { + var caption, code; + + if (example && example.match(/^\s*([\s\S]+?)<\/caption>(\s*[\n\r])([\s\S]+)$/i)) { + caption = RegExp.$1; + code = RegExp.$3; + } + + return { + caption: caption || '', + code: code || example || '' + }; + }); + } + if (doclet.see) { + doclet.see.forEach(function(seeItem, i) { + doclet.see[i] = hashToLink(doclet, seeItem); + }); + } + + // build a list of source files + var sourcePath; + if (doclet.meta) { + sourcePath = getPathFromDoclet(doclet); + sourceFiles[sourcePath] = { + resolved: sourcePath, + shortened: null + }; + if (sourceFilePaths.indexOf(sourcePath) === -1) { + sourceFilePaths.push(sourcePath); + } + } + }); + + // update outdir if necessary, then create outdir + var packageInfo = ( find({kind: 'package'}) || [] ) [0]; + if (packageInfo && packageInfo.name) { + outdir = path.join( outdir, packageInfo.name, (packageInfo.version || '') ); + } + fs.mkPath(outdir); + + // copy the template's static files to outdir + var fromDir = path.join(templatePath, 'static'); + var staticFiles = fs.ls(fromDir, 3); + + staticFiles.forEach(function(fileName) { + var toDir = fs.toDir( fileName.replace(fromDir, outdir) ); + fs.mkPath(toDir); + copyFile(fileName, path.join(toDir, path.basename(fileName)), function(err){if(err) console.err(err);}); + }); + + // copy user-specified static files to outdir + var staticFilePaths; + var staticFileFilter; + var staticFileScanner; + if (conf.default.staticFiles) { + // The canonical property name is `include`. We accept `paths` for backwards compatibility + // with a bug in JSDoc 3.2.x. + staticFilePaths = conf.default.staticFiles.include || + conf.default.staticFiles.paths || + []; + staticFileFilter = new (require('jsdoc/src/filter')).Filter(conf.default.staticFiles); + staticFileScanner = new (require('jsdoc/src/scanner')).Scanner(); + + staticFilePaths.forEach(function(filePath) { + var extraStaticFiles = staticFileScanner.scan([filePath], 10, staticFileFilter); + + extraStaticFiles.forEach(function(fileName) { + var sourcePath = fs.toDir(filePath); + var toDir = fs.toDir( fileName.replace(sourcePath, outdir) ); + fs.mkPath(toDir); + copyFile(fileName, path.join(toDir, path.basename(fileName)), function(err){if(err) console.err(err);}); + }); + }); + } + + if (sourceFilePaths.length) { + sourceFiles = shortenPaths( sourceFiles, path.commonPrefix(sourceFilePaths) ); + } + data().each(function(doclet) { + var url = helper.createLink(doclet); + helper.registerLink(doclet.longname, url); + + // add a shortened version of the full path + var docletPath; + if (doclet.meta) { + docletPath = getPathFromDoclet(doclet); + docletPath = sourceFiles[docletPath].shortened; + if (docletPath) { + doclet.meta.shortpath = docletPath; + } + } + }); + + data().each(function(doclet) { + var url = helper.longnameToUrl[doclet.longname]; + + if (url.indexOf('#') > -1) { + doclet.id = helper.longnameToUrl[doclet.longname].split(/#/).pop(); + } + else { + doclet.id = doclet.name; + } + + if ( needsSignature(doclet) ) { + addSignatureParams(doclet); + addSignatureReturns(doclet); + addAttribs(doclet); + } + }); + + // do this after the urls have all been generated + data().each(function(doclet) { + doclet.ancestors = getAncestorLinks(doclet); + + if (doclet.kind === 'member') { + addSignatureTypes(doclet); + addAttribs(doclet); + } + + if (doclet.kind === 'constant') { + addSignatureTypes(doclet); + addAttribs(doclet); + doclet.kind = 'member'; + } + }); + + var members = helper.getMembers(data); + members.tutorials = tutorials.children; + + // output pretty-printed source files by default + var outputSourceFiles = conf.default && conf.default.outputSourceFiles !== false + ? true + : false; + + // add template helpers + view.find = find; + view.linkto = linkto; + view.resolveAuthorLinks = resolveAuthorLinks; + view.tutoriallink = tutoriallink; + view.htmlsafe = htmlsafe; + view.outputSourceFiles = outputSourceFiles; + + // once for all + view.nav = buildNav(members); + attachModuleSymbols( find({ longname: {left: 'module:'} }), members.modules ); + + // generate the pretty-printed source files first so other pages can link to them + if (outputSourceFiles) { + generateSourceFiles(sourceFiles, opts.encoding); + } + + if (members.globals.length) { + generate('', 'Global', [{kind: 'globalobj'}], globalUrl); + } + + // index page displays information from package.json and lists files + var files = find({kind: 'file'}); + var packages = find({kind: 'package'}); + + generate('', 'High Fidelity API Reference', + packages.concat( + [{kind: 'mainpage', readme: opts.readme, longname: (opts.mainpagetitle) ? opts.mainpagetitle : 'Main Page'}] + ).concat(files), + indexUrl); + + // set up the lists that we'll use to generate pages + var classes = taffy(members.classes); + var modules = taffy(members.modules); + var namespaces = taffy(members.namespaces); + var mixins = taffy(members.mixins); + var externals = taffy(members.externals); + var interfaces = taffy(members.interfaces); + + Object.keys(helper.longnameToUrl).forEach(function(longname) { + var myModules = helper.find(modules, {longname: longname}); + if (myModules.length) { + generate('Module', myModules[0].name, myModules, helper.longnameToUrl[longname]); + } + + var myClasses = helper.find(classes, {longname: longname}); + if (myClasses.length) { + generate('Class', myClasses[0].name, myClasses, helper.longnameToUrl[longname]); + } + + var myNamespaces = helper.find(namespaces, {longname: longname}); + if (myNamespaces.length) { + generate('Namespace', myNamespaces[0].name, myNamespaces, helper.longnameToUrl[longname]); + } + + var myMixins = helper.find(mixins, {longname: longname}); + if (myMixins.length) { + generate('Mixin', myMixins[0].name, myMixins, helper.longnameToUrl[longname]); + } + + var myExternals = helper.find(externals, {longname: longname}); + if (myExternals.length) { + generate('External', myExternals[0].name, myExternals, helper.longnameToUrl[longname]); + } + + var myInterfaces = helper.find(interfaces, {longname: longname}); + if (myInterfaces.length) { + generate('Interface', myInterfaces[0].name, myInterfaces, helper.longnameToUrl[longname]); + } + }); + + // TODO: move the tutorial functions to templateHelper.js + function generateTutorial(title, tutorial, filename) { + var tutorialData = { + title: title, + header: tutorial.title, + content: tutorial.parse(), + children: tutorial.children + }; + + var tutorialPath = path.join(outdir, filename); + var html = view.render('tutorial.tmpl', tutorialData); + + // yes, you can use {@link} in tutorials too! + html = helper.resolveLinks(html); // turn {@link foo} into
      foo + fs.writeFileSync(tutorialPath, html, 'utf8'); + } + + // tutorials can have only one parent so there is no risk for loops + function saveChildren(node) { + node.children.forEach(function(child) { + generateTutorial('Tutorial: ' + child.title, child, helper.tutorialToUrl(child.name)); + saveChildren(child); + }); + } + + saveChildren(tutorials); +}; diff --git a/tools/jsdoc/hifi-jsdoc-template/static/fonts/Cairo-Bold.ttf b/tools/jsdoc/hifi-jsdoc-template/static/fonts/Cairo-Bold.ttf new file mode 100644 index 0000000000..ad884391d7 Binary files /dev/null and b/tools/jsdoc/hifi-jsdoc-template/static/fonts/Cairo-Bold.ttf differ diff --git a/tools/jsdoc/hifi-jsdoc-template/static/fonts/proximanova-regular.otf b/tools/jsdoc/hifi-jsdoc-template/static/fonts/proximanova-regular.otf new file mode 100644 index 0000000000..27c8d8f7bf Binary files /dev/null and b/tools/jsdoc/hifi-jsdoc-template/static/fonts/proximanova-regular.otf differ diff --git a/tools/jsdoc/hifi-jsdoc-template/static/images/white-logo.png b/tools/jsdoc/hifi-jsdoc-template/static/images/white-logo.png new file mode 100644 index 0000000000..18cd2d88dd Binary files /dev/null and b/tools/jsdoc/hifi-jsdoc-template/static/images/white-logo.png differ diff --git a/tools/jsdoc/hifi-jsdoc-template/static/scripts/collapse.js b/tools/jsdoc/hifi-jsdoc-template/static/scripts/collapse.js new file mode 100644 index 0000000000..4525c1756e --- /dev/null +++ b/tools/jsdoc/hifi-jsdoc-template/static/scripts/collapse.js @@ -0,0 +1,11 @@ +function hideAllButCurrent(){ + //by default all submenut items are hidden + $("nav > ul > li > ul li").hide(); + + //only current page (if it exists) should be opened + var file = window.location.pathname.split("/").pop(); + $("nav > ul > li > a[href^='"+file+"']").parent().find("> ul li").show(); +} +$( document ).ready(function() { + hideAllButCurrent(); +}); \ No newline at end of file diff --git a/tools/jsdoc/hifi-jsdoc-template/static/scripts/jquery-3.1.1.min.js b/tools/jsdoc/hifi-jsdoc-template/static/scripts/jquery-3.1.1.min.js new file mode 100644 index 0000000000..4c5be4c0fb --- /dev/null +++ b/tools/jsdoc/hifi-jsdoc-template/static/scripts/jquery-3.1.1.min.js @@ -0,0 +1,4 @@ +/*! jQuery v3.1.1 | (c) jQuery Foundation | jquery.org/license */ +!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.1.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext,B=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,C=/^.[^:#\[\.,]*$/;function D(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):C.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(D(this,a||[],!1))},not:function(a){return this.pushStack(D(this,a||[],!0))},is:function(a){return!!D(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var E,F=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,G=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||E,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:F.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),B.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};G.prototype=r.fn,E=r(d);var H=/^(?:parents|prev(?:Until|All))/,I={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function J(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return J(a,"nextSibling")},prev:function(a){return J(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return a.contentDocument||r.merge([],a.childNodes)}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(I[a]||r.uniqueSort(e),H.test(a)&&e.reverse()),this.pushStack(e)}});var K=/[^\x20\t\r\n\f]+/g;function L(a){var b={};return r.each(a.match(K)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?L(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function M(a){return a}function N(a){throw a}function O(a,b,c){var d;try{a&&r.isFunction(d=a.promise)?d.call(a).done(b).fail(c):a&&r.isFunction(d=a.then)?d.call(a,b,c):b.call(void 0,a)}catch(a){c.call(void 0,a)}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==N&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:M,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:M)),c[2][3].add(g(0,a,r.isFunction(d)?d:N))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(O(a,g.done(h(c)).resolve,g.reject),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)O(e[c],h(c),g.reject);return g.promise()}});var P=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&P.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var Q=r.Deferred();r.fn.ready=function(a){return Q.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,holdReady:function(a){a?r.readyWait++:r.ready(!0)},ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||Q.resolveWith(d,[r]))}}),r.ready.then=Q.then;function R(){d.removeEventListener("DOMContentLoaded",R), +a.removeEventListener("load",R),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",R),a.addEventListener("load",R));var S=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)S(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){W.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=V.get(a,b),c&&(!d||r.isArray(c)?d=V.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return V.get(a,c)||V.access(a,c,{empty:r.Callbacks("once memory").add(function(){V.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,ka=/^$|\/(?:java|ecma)script/i,la={option:[1,""],thead:[1,"","
      "],col:[2,"","
      "],tr:[2,"","
      "],td:[3,"","
      "],_default:[0,"",""]};la.optgroup=la.option,la.tbody=la.tfoot=la.colgroup=la.caption=la.thead,la.th=la.td;function ma(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&r.nodeName(a,b)?r.merge([a],c):c}function na(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=ma(l.appendChild(f),"script"),j&&na(g),c){k=0;while(f=g[k++])ka.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var qa=d.documentElement,ra=/^key/,sa=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ta=/^([^.]*)(?:\.(.+)|)/;function ua(){return!0}function va(){return!1}function wa(){try{return d.activeElement}catch(a){}}function xa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)xa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=va;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(qa,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(K)||[""],j=b.length;while(j--)h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.hasData(a)&&V.get(a);if(q&&(i=q.events)){b=(b||"").match(K)||[""],j=b.length;while(j--)if(h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&V.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(V.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i\x20\t\r\n\f]*)[^>]*)\/>/gi,za=/\s*$/g;function Da(a,b){return r.nodeName(a,"table")&&r.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a:a}function Ea(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Fa(a){var b=Ba.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ga(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(V.hasData(a)&&(f=V.access(a),g=V.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c1&&"string"==typeof q&&!o.checkClone&&Aa.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ia(f,b,c,d)});if(m&&(e=pa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(ma(e,"script"),Ea),i=h.length;l")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=ma(h),f=ma(a),d=0,e=f.length;d0&&na(g,!i&&ma(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(T(c)){if(b=c[V.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[V.expando]=void 0}c[W.expando]&&(c[W.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ja(this,a,!0)},remove:function(a){return Ja(this,a)},text:function(a){return S(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.appendChild(a)}})},prepend:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(ma(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return S(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!za.test(a)&&!la[(ja.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}});function Ya(a,b,c,d,e){return new Ya.prototype.init(a,b,c,d,e)}r.Tween=Ya,Ya.prototype={constructor:Ya,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=Ya.propHooks[this.prop];return a&&a.get?a.get(this):Ya.propHooks._default.get(this)},run:function(a){var b,c=Ya.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Ya.propHooks._default.set(this),this}},Ya.prototype.init.prototype=Ya.prototype,Ya.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},Ya.propHooks.scrollTop=Ya.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=Ya.prototype.init,r.fx.step={};var Za,$a,_a=/^(?:toggle|show|hide)$/,ab=/queueHooks$/;function bb(){$a&&(a.requestAnimationFrame(bb),r.fx.tick())}function cb(){return a.setTimeout(function(){Za=void 0}),Za=r.now()}function db(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ba[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function eb(a,b,c){for(var d,e=(hb.tweeners[b]||[]).concat(hb.tweeners["*"]),f=0,g=e.length;f1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?ib:void 0)), +void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b),null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&r.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(K);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),ib={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=jb[b]||r.find.attr;jb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=jb[g],jb[g]=e,e=null!=c(a,b,d)?g:null,jb[g]=f),e}});var kb=/^(?:input|select|textarea|button)$/i,lb=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return S(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):kb.test(a.nodeName)||lb.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function mb(a){var b=a.match(K)||[];return b.join(" ")}function nb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,nb(this)))});if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,nb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,nb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(K)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=nb(this),b&&V.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":V.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+mb(nb(c))+" ").indexOf(b)>-1)return!0;return!1}});var ob=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":r.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(ob,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:mb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(r.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var pb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!pb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,pb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(V.get(h,"events")||{})[b.type]&&V.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&T(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!T(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=V.access(d,b);e||d.addEventListener(a,c,!0),V.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=V.access(d,b)-1;e?V.access(d,b,e):(d.removeEventListener(a,c,!0),V.remove(d,b))}}});var qb=a.location,rb=r.now(),sb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var tb=/\[\]$/,ub=/\r?\n/g,vb=/^(?:submit|button|image|reset|file)$/i,wb=/^(?:input|select|textarea|keygen)/i;function xb(a,b,c,d){var e;if(r.isArray(b))r.each(b,function(b,e){c||tb.test(a)?d(a,e):xb(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)xb(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(r.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)xb(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&wb.test(this.nodeName)&&!vb.test(a)&&(this.checked||!ia.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:r.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(ub,"\r\n")}}):{name:b.name,value:c.replace(ub,"\r\n")}}).get()}});var yb=/%20/g,zb=/#.*$/,Ab=/([?&])_=[^&]*/,Bb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Cb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Db=/^(?:GET|HEAD)$/,Eb=/^\/\//,Fb={},Gb={},Hb="*/".concat("*"),Ib=d.createElement("a");Ib.href=qb.href;function Jb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(K)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Kb(a,b,c,d){var e={},f=a===Gb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Lb(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Mb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Nb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:qb.href,type:"GET",isLocal:Cb.test(qb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Hb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Lb(Lb(a,r.ajaxSettings),b):Lb(r.ajaxSettings,a)},ajaxPrefilter:Jb(Fb),ajaxTransport:Jb(Gb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Bb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||qb.href)+"").replace(Eb,qb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(K)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Ib.protocol+"//"+Ib.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Kb(Fb,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Db.test(o.type),f=o.url.replace(zb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(yb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(sb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Ab,"$1"),n=(sb.test(f)?"&":"?")+"_="+rb++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Hb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Kb(Gb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Mb(o,y,d)),v=Nb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Ob={0:200,1223:204},Pb=r.ajaxSettings.xhr();o.cors=!!Pb&&"withCredentials"in Pb,o.ajax=Pb=!!Pb,r.ajaxTransport(function(b){var c,d;if(o.cors||Pb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Ob[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r(" + + + + + + + + + + + +
      +

      + + + + +
      + + + +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/jsdoc/hifi-jsdoc-template/tmpl/mainpage.tmpl b/tools/jsdoc/hifi-jsdoc-template/tmpl/mainpage.tmpl new file mode 100644 index 0000000000..b38a185c0f --- /dev/null +++ b/tools/jsdoc/hifi-jsdoc-template/tmpl/mainpage.tmpl @@ -0,0 +1,10 @@ + + + +
      +
      +
      + diff --git a/tools/jsdoc/hifi-jsdoc-template/tmpl/members.tmpl b/tools/jsdoc/hifi-jsdoc-template/tmpl/members.tmpl new file mode 100644 index 0000000000..eef64c1f3f --- /dev/null +++ b/tools/jsdoc/hifi-jsdoc-template/tmpl/members.tmpl @@ -0,0 +1,41 @@ + + + + + + + + + + + + + +
      +
      + Type: +
      + +

      + +

       

      + + + + +
      Example 1? 's':'' ?>
      + + + +
      + + +
      Fires:
      +
        +
      • +
      + + diff --git a/tools/jsdoc/hifi-jsdoc-template/tmpl/method.tmpl b/tools/jsdoc/hifi-jsdoc-template/tmpl/method.tmpl new file mode 100644 index 0000000000..153459f475 --- /dev/null +++ b/tools/jsdoc/hifi-jsdoc-template/tmpl/method.tmpl @@ -0,0 +1,119 @@ + + + + + + + + + + + + + +
      + +
      + Returns: + + + +
      + +

      + + +

      + +

       

      + + + +

      Throws:

      + 1) { ?>
        +
      • +
      +

      + + + + + +

      Parameters

      + + + +
      Example 1? 's':'' ?>
      + + + +
      + + + +
      Extends:
      + + + + +
      Type:
      +
        +
      • + +
      • +
      + + + +
      This:
      +
      + + + +
      Requires:
      +
        +
      • +
      + + + +
      Fires:
      +
        +
      • +
      + + + +
      Listens to Events:
      +
        +
      • +
      + + + +
      Listeners of This Event:
      +
        +
      • +
      + + + +
      Yields:
      + 1) { ?>
        +
      • +
      + + + diff --git a/tools/jsdoc/hifi-jsdoc-template/tmpl/methodList.tmpl b/tools/jsdoc/hifi-jsdoc-template/tmpl/methodList.tmpl new file mode 100644 index 0000000000..7d88cd5e51 --- /dev/null +++ b/tools/jsdoc/hifi-jsdoc-template/tmpl/methodList.tmpl @@ -0,0 +1,107 @@ + + + +

      Constructor

      + + + + + + + + + + None + + 1) { ?>
        +
      • +
      + + + + + + + + + + + + + + + + + +
      Extends:
      + + + + +
      Type:
      +
        +
      • + +
      • +
      + + + +
      This:
      +
      + + + + +
      Requires:
      +
        +
      • +
      + + + +
      Fires:
      +
        +
      • +
      + + + +
      Listens to Events:
      +
        +
      • +
      + + + +
      Listeners of This Event:
      +
        +
      • +
      + + + +
      Yields:
      + 1) { ?>
        +
      • +
      + + + + \ No newline at end of file diff --git a/tools/jsdoc/hifi-jsdoc-template/tmpl/paramList.tmpl b/tools/jsdoc/hifi-jsdoc-template/tmpl/paramList.tmpl new file mode 100644 index 0000000000..5b5164b771 --- /dev/null +++ b/tools/jsdoc/hifi-jsdoc-template/tmpl/paramList.tmpl @@ -0,0 +1,67 @@ + + + + + + + , + + + diff --git a/tools/jsdoc/hifi-jsdoc-template/tmpl/params.tmpl b/tools/jsdoc/hifi-jsdoc-template/tmpl/params.tmpl new file mode 100644 index 0000000000..17f58b1677 --- /dev/null +++ b/tools/jsdoc/hifi-jsdoc-template/tmpl/params.tmpl @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameTypeAttributesDefaultDescription
      + + + + + + Default Value: + + +
      Properties
      + +
      \ No newline at end of file diff --git a/tools/jsdoc/hifi-jsdoc-template/tmpl/properties.tmpl b/tools/jsdoc/hifi-jsdoc-template/tmpl/properties.tmpl new file mode 100644 index 0000000000..5c83a7d587 --- /dev/null +++ b/tools/jsdoc/hifi-jsdoc-template/tmpl/properties.tmpl @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameTypeAttributesSummary
      + + + + + + <optional>
      + + + + <nullable>
      + +
      + + + +

      Default Value:

      + + + +
      Properties
      + +
      diff --git a/tools/jsdoc/hifi-jsdoc-template/tmpl/returns.tmpl b/tools/jsdoc/hifi-jsdoc-template/tmpl/returns.tmpl new file mode 100644 index 0000000000..3958e7018e --- /dev/null +++ b/tools/jsdoc/hifi-jsdoc-template/tmpl/returns.tmpl @@ -0,0 +1,8 @@ + + + + + diff --git a/tools/jsdoc/hifi-jsdoc-template/tmpl/returnsSimp.tmpl b/tools/jsdoc/hifi-jsdoc-template/tmpl/returnsSimp.tmpl new file mode 100644 index 0000000000..b93aa50ad4 --- /dev/null +++ b/tools/jsdoc/hifi-jsdoc-template/tmpl/returnsSimp.tmpl @@ -0,0 +1,6 @@ + + + + + diff --git a/tools/jsdoc/hifi-jsdoc-template/tmpl/signal.tmpl b/tools/jsdoc/hifi-jsdoc-template/tmpl/signal.tmpl new file mode 100644 index 0000000000..20743fe052 --- /dev/null +++ b/tools/jsdoc/hifi-jsdoc-template/tmpl/signal.tmpl @@ -0,0 +1,123 @@ + + + + + + + + + + + + + +
      + ( + + + + ) +
      + Returns: + + + +
      + +

      + + +

      + +

       

      + + + +

      Throws:

      + 1) { ?>
        +
      • +
      +

      + + + + + +

      Parameters

      + + + +
      Example 1? 's':'' ?>
      + + + +
      + + + +
      Extends:
      + + + + +
      Type:
      +
        +
      • + +
      • +
      + + + +
      This:
      +
      + + + +
      Requires:
      +
        +
      • +
      + + + +
      Fires:
      +
        +
      • +
      + + + +
      Listens to Events:
      +
        +
      • +
      + + + +
      Listeners of This Event:
      +
        +
      • +
      + + + +
      Yields:
      + 1) { ?>
        +
      • +
      + + + diff --git a/tools/jsdoc/hifi-jsdoc-template/tmpl/signalList.tmpl b/tools/jsdoc/hifi-jsdoc-template/tmpl/signalList.tmpl new file mode 100644 index 0000000000..b9a0e0ca86 --- /dev/null +++ b/tools/jsdoc/hifi-jsdoc-template/tmpl/signalList.tmpl @@ -0,0 +1,92 @@ + + + +

      Constructor

      + + + + + + + + + + + + + + + + + + + + +
      Extends:
      + + + + +
      Type:
      +
        +
      • + +
      • +
      + + + +
      This:
      +
      + + + + +
      Requires:
      +
        +
      • +
      + + + +
      Fires:
      +
        +
      • +
      + + + +
      Listens to Events:
      +
        +
      • +
      + + + +
      Listeners of This Event:
      +
        +
      • +
      + + + +
      Yields:
      + 1) { ?>
        +
      • +
      + + + + \ No newline at end of file diff --git a/tools/jsdoc/hifi-jsdoc-template/tmpl/source.tmpl b/tools/jsdoc/hifi-jsdoc-template/tmpl/source.tmpl new file mode 100644 index 0000000000..e559b5d103 --- /dev/null +++ b/tools/jsdoc/hifi-jsdoc-template/tmpl/source.tmpl @@ -0,0 +1,8 @@ + +
      +
      +
      +
      +
      \ No newline at end of file diff --git a/tools/jsdoc/hifi-jsdoc-template/tmpl/tutorial.tmpl b/tools/jsdoc/hifi-jsdoc-template/tmpl/tutorial.tmpl new file mode 100644 index 0000000000..88a0ad52aa --- /dev/null +++ b/tools/jsdoc/hifi-jsdoc-template/tmpl/tutorial.tmpl @@ -0,0 +1,19 @@ +
      + +
      + 0) { ?> +
        +
      • +
      + + +

      +
      + +
      + +
      + +
      diff --git a/tools/jsdoc/hifi-jsdoc-template/tmpl/type.tmpl b/tools/jsdoc/hifi-jsdoc-template/tmpl/type.tmpl new file mode 100644 index 0000000000..8f8d89369d --- /dev/null +++ b/tools/jsdoc/hifi-jsdoc-template/tmpl/type.tmpl @@ -0,0 +1,7 @@ + + +| + \ No newline at end of file diff --git a/tools/jsdoc/out/README.md b/tools/jsdoc/out/README.md new file mode 100644 index 0000000000..3f9519555e --- /dev/null +++ b/tools/jsdoc/out/README.md @@ -0,0 +1 @@ +This is the output directory for the JavaScript API documentation generated by jsdoc. \ No newline at end of file diff --git a/tools/jsdoc/plugins/hifi.js b/tools/jsdoc/plugins/hifi.js index a525093965..b4350ddbdb 100644 --- a/tools/jsdoc/plugins/hifi.js +++ b/tools/jsdoc/plugins/hifi.js @@ -107,9 +107,6 @@ exports.handlers = { if (e.doclet.hifiClientEntity) { rows.push("Client Entity Scripts"); } - if (e.doclet.hifiAvatar) { - rows.push("Avatar Scripts"); - } if (e.doclet.hifiServerEntity) { rows.push("Server Entity Scripts"); } @@ -117,15 +114,30 @@ exports.handlers = { rows.push("Assignment Client Scripts"); } - // Append an Available In: table at the end of the namespace description. + // Append an Available In: sentence at the beginning of the namespace description. if (rows.length > 0) { - var table = "
      Available in:" + rows.join("") + "

      "; - e.doclet.description = table + (e.doclet.description ? e.doclet.description : ""); - } + var availableIn = "

      Supported Script Types: " + rows.join(" • ") + "

      "; + + e.doclet.description = (e.doclet.description ? e.doclet.description : "") + availableIn; + } } } }; +// Functions for adding @signal custom tag +/** @private */ +function setDocletKindToTitle(doclet, tag) { + doclet.addTag( 'kind', tag.title ); +} + +function setDocletNameToValue(doclet, tag) { + if (tag.value && tag.value.description) { // as in a long tag + doclet.addTag('name', tag.value.description); + } else if (tag.text) { // or a short tag + doclet.addTag('name', tag.text); + } +} + // Define custom hifi tags here exports.defineTags = function (dictionary) { @@ -143,14 +155,6 @@ exports.defineTags = function (dictionary) { } }); - // @hifi-avatar-script - dictionary.defineTag("hifi-avatar", { - onTagged: function (doclet, tag) { - doclet.hifiAvatar = true; - } - }); - - // @hifi-client-entity dictionary.defineTag("hifi-client-entity", { onTagged: function (doclet, tag) { @@ -164,4 +168,14 @@ exports.defineTags = function (dictionary) { doclet.hifiServerEntity = true; } }); + + // @signal + dictionary.defineTag("signal", { + mustHaveValue: true, + onTagged: function(doclet, tag) { + setDocletKindToTitle(doclet, tag); + setDocletNameToValue(doclet, tag); + } + }); + }; \ No newline at end of file diff --git a/tools/nitpick/CMakeLists.txt b/tools/nitpick/CMakeLists.txt index e69b16b866..44eace5e70 100644 --- a/tools/nitpick/CMakeLists.txt +++ b/tools/nitpick/CMakeLists.txt @@ -80,7 +80,9 @@ else () add_executable(${TARGET_NAME} ${NITPICK_SRCS} ${QM}) endif () -add_dependencies(${TARGET_NAME} resources) +if (NOT UNIX) + add_dependencies(${TARGET_NAME} resources) +endif() # disable /OPT:REF and /OPT:ICF for the Debug builds # This will prevent the following linker warnings diff --git a/tools/nitpick/README.md b/tools/nitpick/README.md index c7b9050070..b4bcff81c8 100644 --- a/tools/nitpick/README.md +++ b/tools/nitpick/README.md @@ -47,24 +47,30 @@ These steps assume the hifi repository has been cloned to `~/hifi`. ### Windows 1. (First time) download and install Python 3 from https://hifi-qa.s3.amazonaws.com/nitpick/Windows/python-3.7.0-amd64.exe (also located at https://www.python.org/downloads/) 1. Click the "add python to path" checkbox on the python installer - 1. After installation - add the path to python.exe to the Windows PATH environment variable. + 1. After installation: + 1. Open a new terminal + 1. Enter `python` and hit enter + 1. Verify that python is available (the prompt will change to `>>>`) + 1. Type `exit()` and hit enter to close python + 1. Install requests (a python library to download files from URLs) + `pip3 install requests` 1. (First time) download and install AWS CLI from https://hifi-qa.s3.amazonaws.com/nitpick/Windows/AWSCLI64PY3.msi (also available at https://aws.amazon.com/cli/ 1. Open a new command prompt and run `aws configure` 1. Enter the AWS account number 1. Enter the secret key 1. Leave region name and ouput format as default [None] - 1. Install the latest release of Boto3 via pip: + 1. Install the latest release of Boto3 via pip (from a terminal): `pip install boto3` 1. (First time) Download adb (Android Debug Bridge) from *https://dl.google.com/android/repository/platform-tools-latest-windows.zip* 1. Copy the downloaded file to (for example) **C:\adb** and extract in place. Verify you see *adb.exe* in **C:\adb\platform-tools\\**. 1. After installation - add the path to adb.exe to the Windows PATH environment variable (note that it is in *adb\platform-tools*). -1. `nitpick` is included in the High Fidelity installer but can also be downloaded from: +1. `nitpick` is included in the High Fidelity installer but can also be downloaded from (change X.X.X to correct version): [here]().* ### Mac -1. (first time) Install brew +1. (First time) Install brew In a terminal: `/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"` Note that you will need to press RETURN again, and will then be asked for your password. @@ -76,11 +82,13 @@ These steps assume the hifi repository has been cloned to `~/hifi`. `open "/Applications/Python 3.7/Install Certificates.command"`. This is needed because the Mac Python supplied no longer links with the deprecated Apple-supplied system OpenSSL libraries but rather supplies a private copy of OpenSSL 1.0.2 which does not automatically access the system default root certificates. 1. Verify that `/usr/local/bin/python3` exists. -1. (First time - AWS interface) Install pip with the script provided by the Python Packaging Authority: In a terminal: `curl -O https://bootstrap.pypa.io/get-pip.py` In a terminal: `python3 get-pip.py --user` + 1. Install requests (a python library to download files from URLs) + `pip3 install requests` +1. (First time - AWS interface) Install pip with the script provided by the Python Packaging Authority: 1. Use pip to install the AWS CLI. `pip3 install awscli --upgrade --user` This will install aws in your user. For user XXX, aws will be located in ~/Library/Python/3.7/bin @@ -92,6 +100,16 @@ This is needed because the Mac Python supplied no longer links with the deprecat 1. Install the latest release of Boto3 via pip: pip3 install boto3 1. (First time)Install adb (the Android Debug Bridge) - in a terminal: `brew cask install android-platform-tools` +1. (First time) Set terminal privileges + 1. Click on Apple icon (top left) + 1. Select System Preferences... + 1. Select Security & Privacy + 1. Select Accessibility + 1. Click on "Click the lock to make changes" and enter passsword if requested + 1. Set Checkbox near *Terminal* to checked. + 1. Click on "Click the lock to prevent furthur changes" + 1. Close window + 1. `nitpick` is included in the High Fidelity installer but can also be downloaded from: [here]().* # Usage diff --git a/tools/nitpick/src/AWSInterface.cpp b/tools/nitpick/src/AWSInterface.cpp index 4e83460b9e..a098d17917 100644 --- a/tools/nitpick/src/AWSInterface.cpp +++ b/tools/nitpick/src/AWSInterface.cpp @@ -27,7 +27,10 @@ AWSInterface::AWSInterface(QObject* parent) : QObject(parent) { void AWSInterface::createWebPageFromResults(const QString& testResults, const QString& workingDirectory, QCheckBox* updateAWSCheckBox, - QLineEdit* urlLineEdit) { + QRadioButton* diffImageRadioButton, + QRadioButton* ssimImageRadionButton, + QLineEdit* urlLineEdit +) { _workingDirectory = workingDirectory; // Verify filename is in correct format @@ -52,6 +55,13 @@ void AWSInterface::createWebPageFromResults(const QString& testResults, QString zipFilenameWithoutExtension = zipFilename.split('.')[0]; extractTestFailuresFromZippedFolder(_workingDirectory + "/" + zipFilenameWithoutExtension); + + if (diffImageRadioButton->isChecked()) { + _comparisonImageFilename = "Difference Image.png"; + } else { + _comparisonImageFilename = "SSIM Image.png"; + } + createHTMLFile(); if (updateAWSCheckBox->isChecked()) { @@ -353,7 +363,7 @@ void AWSInterface::openTable(QTextStream& stream, const QString& testResult, con stream << "\t\t\t\t

      Test

      \n"; stream << "\t\t\t\t

      Actual Image

      \n"; stream << "\t\t\t\t

      Expected Image

      \n"; - stream << "\t\t\t\t

      Difference Image

      \n"; + stream << "\t\t\t\t

      Comparison Image

      \n"; stream << "\t\t\t\n"; } } @@ -378,12 +388,13 @@ void AWSInterface::createEntry(const int index, const QString& testResult, QText QString folder; bool differenceFileFound; + if (isFailure) { folder = FAILURES_FOLDER; - differenceFileFound = QFile::exists(_htmlFailuresFolder + "/" + resultName + "/Difference Image.png"); + differenceFileFound = QFile::exists(_htmlFailuresFolder + "/" + resultName + "/" + _comparisonImageFilename); } else { folder = SUCCESSES_FOLDER; - differenceFileFound = QFile::exists(_htmlSuccessesFolder + "/" + resultName + "/Difference Image.png"); + differenceFileFound = QFile::exists(_htmlSuccessesFolder + "/" + resultName + "/" + _comparisonImageFilename); } if (textResultsFileFound) { @@ -450,7 +461,7 @@ void AWSInterface::createEntry(const int index, const QString& testResult, QText stream << "\t\t\t\t\n"; if (differenceFileFound) { - stream << "\t\t\t\t\n"; + stream << "\t\t\t\t\n"; } else { stream << "\t\t\t\t

      No Image Found

      \n"; } @@ -469,7 +480,7 @@ void AWSInterface::updateAWS() { if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), - "Could not create 'addTestCases.py'"); + "Could not create 'updateAWS.py'"); exit(-1); } @@ -512,12 +523,12 @@ void AWSInterface::updateAWS() { stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Expected Image.png" << "', Body=data)\n\n"; - if (QFile::exists(_htmlFailuresFolder + "/" + parts[parts.length() - 1] + "/Difference Image.png")) { + if (QFile::exists(_htmlFailuresFolder + "/" + parts[parts.length() - 1] + "/" + _comparisonImageFilename)) { stream << "data = open('" << _workingDirectory << "/" << filename << "/" - << "Difference Image.png" + << _comparisonImageFilename << "', 'rb')\n"; - stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Difference Image.png" << "', Body=data)\n\n"; + stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << _comparisonImageFilename << "', Body=data)\n\n"; } } } @@ -555,12 +566,12 @@ void AWSInterface::updateAWS() { stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Expected Image.png" << "', Body=data)\n\n"; - if (QFile::exists(_htmlSuccessesFolder + "/" + parts[parts.length() - 1] + "/Difference Image.png")) { + if (QFile::exists(_htmlSuccessesFolder + "/" + parts[parts.length() - 1] + "/" + _comparisonImageFilename)) { stream << "data = open('" << _workingDirectory << "/" << filename << "/" - << "Difference Image.png" + << _comparisonImageFilename << "', 'rb')\n"; - stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << "Difference Image.png" << "', Body=data)\n\n"; + stream << "s3.Bucket('hifi-content').put_object(Bucket='" << AWS_BUCKET << "', Key='" << filename << "/" << _comparisonImageFilename << "', Body=data)\n\n"; } } } @@ -578,6 +589,7 @@ void AWSInterface::updateAWS() { QProcess* process = new QProcess(); + _busyWindow.setWindowTitle("Updating AWS"); connect(process, &QProcess::started, this, [=]() { _busyWindow.exec(); }); connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater())); connect(process, static_cast(&QProcess::finished), this, diff --git a/tools/nitpick/src/AWSInterface.h b/tools/nitpick/src/AWSInterface.h index d95b8ecf2f..77d500fa7c 100644 --- a/tools/nitpick/src/AWSInterface.h +++ b/tools/nitpick/src/AWSInterface.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include "BusyWindow.h" @@ -28,6 +29,8 @@ public: void createWebPageFromResults(const QString& testResults, const QString& workingDirectory, QCheckBox* updateAWSCheckBox, + QRadioButton* diffImageRadioButton, + QRadioButton* ssimImageRadionButton, QLineEdit* urlLineEdit); void extractTestFailuresFromZippedFolder(const QString& folderName); @@ -67,6 +70,9 @@ private: QString AWS_BUCKET{ "hifi-qa" }; QLineEdit* _urlLineEdit; + + + QString _comparisonImageFilename; }; #endif // hifi_AWSInterface_h \ No newline at end of file diff --git a/tools/nitpick/src/AdbInterface.cpp b/tools/nitpick/src/AdbInterface.cpp index 82ef1446e3..41eb947efa 100644 --- a/tools/nitpick/src/AdbInterface.cpp +++ b/tools/nitpick/src/AdbInterface.cpp @@ -16,12 +16,13 @@ QString AdbInterface::getAdbCommand() { #ifdef Q_OS_WIN if (_adbCommand.isNull()) { - QString adbPath = PathUtils::getPathToExecutable("adb.exe"); + QString adbExe{ "adb.exe" }; + QString adbPath = PathUtils::getPathToExecutable(adbExe); if (!adbPath.isNull()) { - _adbCommand = adbPath + _adbExe; + _adbCommand = adbExe; } else { - QMessageBox::critical(0, "python.exe not found", - "Please verify that pyton.exe is in the PATH"); + QMessageBox::critical(0, "adb.exe not found", + "Please verify that adb.exe is in the PATH"); exit(-1); } } diff --git a/tools/nitpick/src/AdbInterface.h b/tools/nitpick/src/AdbInterface.h index c1ce84c019..a2aa2be8ea 100644 --- a/tools/nitpick/src/AdbInterface.h +++ b/tools/nitpick/src/AdbInterface.h @@ -17,12 +17,6 @@ public: QString getAdbCommand(); private: -#ifdef Q_OS_WIN - const QString _adbExe{ "adb.exe" }; -#else - // Both Mac and Linux use "python" - const QString _adbExe{ "adb" }; -#endif QString _adbCommand; }; diff --git a/tools/nitpick/src/Downloader.cpp b/tools/nitpick/src/Downloader.cpp index 3256e79601..61847fe5bc 100644 --- a/tools/nitpick/src/Downloader.cpp +++ b/tools/nitpick/src/Downloader.cpp @@ -8,32 +8,66 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include "Downloader.h" +#include "PythonInterface.h" -#include +#include +#include +#include +#include +#include -Downloader::Downloader(QUrl fileURL, QObject *parent) : QObject(parent) { - _networkAccessManager.get(QNetworkRequest(fileURL)); - - connect( - &_networkAccessManager, SIGNAL (finished(QNetworkReply*)), - this, SLOT (fileDownloaded(QNetworkReply*)) - ); +Downloader::Downloader() { + PythonInterface* pythonInterface = new PythonInterface(); + _pythonCommand = pythonInterface->getPythonCommand(); } -void Downloader::fileDownloaded(QNetworkReply* reply) { - QNetworkReply::NetworkError error = reply->error(); - if (error != QNetworkReply::NetworkError::NoError) { - QMessageBox::information(0, "Test Aborted", "Failed to download file: " + reply->errorString()); +void Downloader::downloadFiles(const QStringList& URLs, const QString& directoryName, const QStringList& filenames, void *caller) { + if (URLs.size() <= 0) { return; } - _downloadedData = reply->readAll(); + QString filename = directoryName + "/downloadFiles.py"; + if (QFile::exists(filename)) { + QFile::remove(filename); + } + QFile file(filename); - //emit a signal - reply->deleteLater(); - emit downloaded(); -} + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Could not create 'downloadFiles.py'"); + exit(-1); + } -QByteArray Downloader::downloadedData() const { - return _downloadedData; + QTextStream stream(&file); + + stream << "import requests\n"; + + for (int i = 0; i < URLs.size(); ++i) { + stream << "\nurl = '" + URLs[i] + "'\n"; + stream << "r = requests.get(url)\n"; + stream << "open('" + directoryName + '/' + filenames [i] + "', 'wb').write(r.content)\n"; + } + + file.close(); + +#ifdef Q_OS_WIN + QProcess* process = new QProcess(); + _busyWindow.setWindowTitle("Downloading Files"); + connect(process, &QProcess::started, this, [=]() { _busyWindow.exec(); }); + connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater())); + connect(process, static_cast(&QProcess::finished), this, + [=](int exitCode, QProcess::ExitStatus exitStatus) { _busyWindow.hide(); }); + + QStringList parameters = QStringList() << filename; + process->start(_pythonCommand, parameters); +#elif defined Q_OS_MAC + QProcess* process = new QProcess(); + QStringList parameters = QStringList() << "-c" << _pythonCommand + " " + filename; + process->start("sh", parameters); + + // Wait for the last file to download + while (!QFile::exists(directoryName + '/' + filenames[filenames.length() - 1])) { + QThread::msleep(200); + } +#endif } diff --git a/tools/nitpick/src/Downloader.h b/tools/nitpick/src/Downloader.h index 742a88b890..e48c195999 100644 --- a/tools/nitpick/src/Downloader.h +++ b/tools/nitpick/src/Downloader.h @@ -11,38 +11,19 @@ #ifndef hifi_downloader_h #define hifi_downloader_h -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include "BusyWindow.h" #include -#include -#include -#include -#include - class Downloader : public QObject { Q_OBJECT public: - explicit Downloader(QUrl fileURL, QObject *parent = 0); + Downloader(); - QByteArray downloadedData() const; - -signals: - void downloaded(); - -private slots: - void fileDownloaded(QNetworkReply* pReply); + void downloadFiles(const QStringList& URLs, const QString& directoryName, const QStringList& filenames, void *caller); private: - QNetworkAccessManager _networkAccessManager; - QByteArray _downloadedData; + QString _pythonCommand; + BusyWindow _busyWindow; }; #endif // hifi_downloader_h \ No newline at end of file diff --git a/tools/nitpick/src/ImageComparer.cpp b/tools/nitpick/src/ImageComparer.cpp index fa73f97887..7e3e6eaf63 100644 --- a/tools/nitpick/src/ImageComparer.cpp +++ b/tools/nitpick/src/ImageComparer.cpp @@ -14,7 +14,7 @@ // Computes SSIM - see https://en.wikipedia.org/wiki/Structural_similarity // The value is computed for the luminance component and the average value is returned -double ImageComparer::compareImages(QImage resultImage, QImage expectedImage) const { +void ImageComparer::compareImages(const QImage& resultImage, const QImage& expectedImage) { const int L = 255; // (2^number of bits per pixel) - 1 const double K1 { 0.01 }; @@ -39,8 +39,13 @@ double ImageComparer::compareImages(QImage resultImage, QImage expectedImage) co double p[WIN_SIZE * WIN_SIZE]; double q[WIN_SIZE * WIN_SIZE]; + _ssimResults.results.clear(); + int windowCounter{ 0 }; double ssim{ 0.0 }; + double min { 1.0 }; + double max { -1.0 }; + while (x < expectedImage.width()) { int lastX = x + WIN_SIZE - 1; if (lastX > expectedImage.width() - 1) { @@ -96,7 +101,13 @@ double ImageComparer::compareImages(QImage resultImage, QImage expectedImage) co double numerator = (2.0 * mP * mQ + c1) * (2.0 * sigPQ + c2); double denominator = (mP * mP + mQ * mQ + c1) * (sigsqP + sigsqQ + c2); - ssim += numerator / denominator; + double value { numerator / denominator }; + _ssimResults.results.push_back(value); + ssim += value; + + if (value < min) min = value; + if (value > max) max = value; + ++windowCounter; y += WIN_SIZE; @@ -106,5 +117,17 @@ double ImageComparer::compareImages(QImage resultImage, QImage expectedImage) co y = 0; } - return ssim / windowCounter; -}; \ No newline at end of file + _ssimResults.width = (int)(expectedImage.width() / WIN_SIZE); + _ssimResults.height = (int)(expectedImage.height() / WIN_SIZE); + _ssimResults.min = min; + _ssimResults.max = max; + _ssimResults.ssim = ssim / windowCounter; +}; + +double ImageComparer::getSSIMValue() { + return _ssimResults.ssim; +} + +SSIMResults ImageComparer::getSSIMResults() { + return _ssimResults; +} diff --git a/tools/nitpick/src/ImageComparer.h b/tools/nitpick/src/ImageComparer.h index 7b7b8b0b74..fc14dab94d 100644 --- a/tools/nitpick/src/ImageComparer.h +++ b/tools/nitpick/src/ImageComparer.h @@ -10,12 +10,20 @@ #ifndef hifi_ImageComparer_h #define hifi_ImageComparer_h +#include "common.h" + #include #include class ImageComparer { public: - double compareImages(QImage resultImage, QImage expectedImage) const; + void compareImages(const QImage& resultImage, const QImage& expectedImage); + double getSSIMValue(); + + SSIMResults getSSIMResults(); + +private: + SSIMResults _ssimResults; }; #endif // hifi_ImageComparer_h diff --git a/tools/nitpick/src/MismatchWindow.cpp b/tools/nitpick/src/MismatchWindow.cpp index 58189b4795..fd5df0dd4e 100644 --- a/tools/nitpick/src/MismatchWindow.cpp +++ b/tools/nitpick/src/MismatchWindow.cpp @@ -21,7 +21,7 @@ MismatchWindow::MismatchWindow(QWidget *parent) : QDialog(parent) { diffImage->setScaledContents(true); } -QPixmap MismatchWindow::computeDiffPixmap(QImage expectedImage, QImage resultImage) { +QPixmap MismatchWindow::computeDiffPixmap(const QImage& expectedImage, const QImage& resultImage) { // Create an empty difference image if the images differ in size if (expectedImage.height() != resultImage.height() || expectedImage.width() != resultImage.width()) { return QPixmap(); @@ -60,7 +60,7 @@ QPixmap MismatchWindow::computeDiffPixmap(QImage expectedImage, QImage resultIma return resultPixmap; } -void MismatchWindow::setTestResult(TestResult testResult) { +void MismatchWindow::setTestResult(const TestResult& testResult) { errorLabel->setText("Similarity: " + QString::number(testResult._error)); imagePath->setText("Path to test: " + testResult._pathname); @@ -99,3 +99,36 @@ void MismatchWindow::on_abortTestsButton_clicked() { QPixmap MismatchWindow::getComparisonImage() { return _diffPixmap; } + +QPixmap MismatchWindow::getSSIMResultsImage(const SSIMResults& ssimResults) { + // This is an optimization, as QImage.setPixel() is embarrassingly slow + const int ELEMENT_SIZE { 8 }; + const int WIDTH{ ssimResults.width * ELEMENT_SIZE }; + const int HEIGHT{ ssimResults.height * ELEMENT_SIZE }; + + unsigned char* buffer = new unsigned char[WIDTH * HEIGHT * 3]; + + + // loop over each SSIM result + for (int y = 0; y < ssimResults.height; ++y) { + for (int x = 0; x < ssimResults.width; ++x) { + double scaledResult = (ssimResults.results[x * ssimResults.height + y] + 1.0) / (2.0); + //double scaledResult = (ssimResults.results[x * ssimResults.height + y] - ssimResults.min) / (ssimResults.max - ssimResults.min); + // Create a square + for (int yy = 0; yy < ELEMENT_SIZE; ++yy) { + for (int xx = 0; xx < ELEMENT_SIZE; ++xx) { + buffer[(xx + yy * WIDTH + x * ELEMENT_SIZE + y * WIDTH * ELEMENT_SIZE) * 3 + 0] = 255 * (1.0 - scaledResult); // R + buffer[(xx + yy * WIDTH + x * ELEMENT_SIZE + y * WIDTH * ELEMENT_SIZE) * 3 + 1] = 255 * scaledResult; // G + buffer[(xx + yy * WIDTH + x * ELEMENT_SIZE + y * WIDTH * ELEMENT_SIZE) * 3 + 2] = 0; // B + } + } + } + } + + QImage image(buffer, WIDTH, HEIGHT, QImage::Format_RGB888); + QPixmap pixmap = QPixmap::fromImage(image); + + delete[] buffer; + + return pixmap; +} diff --git a/tools/nitpick/src/MismatchWindow.h b/tools/nitpick/src/MismatchWindow.h index 040e0b8bf1..116d35dfc5 100644 --- a/tools/nitpick/src/MismatchWindow.h +++ b/tools/nitpick/src/MismatchWindow.h @@ -20,12 +20,14 @@ class MismatchWindow : public QDialog, public Ui::MismatchWindow { public: MismatchWindow(QWidget *parent = Q_NULLPTR); - void setTestResult(TestResult testResult); + void setTestResult(const TestResult& testResult); UserResponse getUserResponse() { return _userResponse; } - QPixmap computeDiffPixmap(QImage expectedImage, QImage resultImage); + QPixmap computeDiffPixmap(const QImage& expectedImage, const QImage& resultImage); + QPixmap getComparisonImage(); + QPixmap getSSIMResultsImage(const SSIMResults& ssimResults); private slots: void on_passTestButton_clicked(); diff --git a/tools/nitpick/src/Nitpick.cpp b/tools/nitpick/src/Nitpick.cpp index 39800c6bc6..cf50774617 100644 --- a/tools/nitpick/src/Nitpick.cpp +++ b/tools/nitpick/src/Nitpick.cpp @@ -24,8 +24,6 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) { _ui.progressBar->setVisible(false); _ui.tabWidget->setCurrentIndex(0); - _signalMapper = new QSignalMapper(); - connect(_ui.actionClose, &QAction::triggered, this, &Nitpick::on_closePushbutton_clicked); connect(_ui.actionAbout, &QAction::triggered, this, &Nitpick::about); connect(_ui.actionContent, &QAction::triggered, this, &Nitpick::content); @@ -40,7 +38,7 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) { _ui.plainTextEdit->setReadOnly(true); - setWindowTitle("Nitpick - v3.0.0"); + setWindowTitle("Nitpick - v3.1.2"); clientProfiles << "VR-High" << "Desktop-High" << "Desktop-Low" << "Mobile-Touch" << "VR-Standalone"; _ui.clientProfileComboBox->insertItems(0, clientProfiles); @@ -48,10 +46,8 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) { } Nitpick::~Nitpick() { - delete _signalMapper; - - if (_test) { - delete _test; + if (_testCreator) { + delete _testCreator; } if (_testRunnerDesktop) { @@ -64,10 +60,10 @@ Nitpick::~Nitpick() { } void Nitpick::setup() { - if (_test) { - delete _test; + if (_testCreator) { + delete _testCreator; } - _test = new Test(_ui.progressBar, _ui.checkBoxInteractiveMode); + _testCreator = new TestCreator(_ui.progressBar, _ui.checkBoxInteractiveMode); std::vector dayCheckboxes; dayCheckboxes.emplace_back(_ui.mondayCheckBox); @@ -99,9 +95,12 @@ void Nitpick::setup() { timeEditCheckboxes, timeEdits, _ui.workingFolderRunOnDesktopLabel, - _ui.checkBoxServerless, + _ui.checkBoxServerless, + _ui.usePreviousInstallationOnDesktopCheckBox, _ui.runLatestOnDesktopCheckBox, _ui.urlOnDesktopLineEdit, + _ui.runFullSuiteOnDesktopCheckBox, + _ui.scriptURLOnDesktopLineEdit, _ui.runNowPushbutton, _ui.statusLabelOnDesktop ); @@ -118,8 +117,11 @@ void Nitpick::setup() { _ui.downloadAPKPushbutton, _ui.installAPKPushbutton, _ui.runInterfacePushbutton, + _ui.usePreviousInstallationOnMobileCheckBox, _ui.runLatestOnMobileCheckBox, _ui.urlOnMobileLineEdit, + _ui.runFullSuiteOnMobileCheckBox, + _ui.scriptURLOnMobileLineEdit, _ui.statusLabelOnMobile ); } @@ -130,7 +132,7 @@ void Nitpick::startTestsEvaluation(const bool isRunningFromCommandLine, const QString& branch, const QString& user ) { - _test->startTestsEvaluation(isRunningFromCommandLine, isRunningInAutomaticTestRun, snapshotDirectory, branch, user); + _testCreator->startTestsEvaluation(isRunningFromCommandLine, isRunningInAutomaticTestRun, snapshotDirectory, branch, user); } void Nitpick::on_tabWidget_currentChanged(int index) { @@ -148,48 +150,44 @@ void Nitpick::on_tabWidget_currentChanged(int index) { } } -void Nitpick::on_evaluateTestsPushbutton_clicked() { - _test->startTestsEvaluation(false, false); -} - void Nitpick::on_createRecursiveScriptPushbutton_clicked() { - _test->createRecursiveScript(); + _testCreator->createRecursiveScript(); } void Nitpick::on_createAllRecursiveScriptsPushbutton_clicked() { - _test->createAllRecursiveScripts(); + _testCreator->createAllRecursiveScripts(); } void Nitpick::on_createTestsPushbutton_clicked() { - _test->createTests(_ui.clientProfileComboBox->currentText()); + _testCreator->createTests(_ui.clientProfileComboBox->currentText()); } void Nitpick::on_createMDFilePushbutton_clicked() { - _test->createMDFile(); + _testCreator->createMDFile(); } void Nitpick::on_createAllMDFilesPushbutton_clicked() { - _test->createAllMDFiles(); + _testCreator->createAllMDFiles(); } void Nitpick::on_createTestAutoScriptPushbutton_clicked() { - _test->createTestAutoScript(); + _testCreator->createTestAutoScript(); } void Nitpick::on_createAllTestAutoScriptsPushbutton_clicked() { - _test->createAllTestAutoScripts(); + _testCreator->createAllTestAutoScripts(); } void Nitpick::on_createTestsOutlinePushbutton_clicked() { - _test->createTestsOutline(); + _testCreator->createTestsOutline(); } void Nitpick::on_createTestRailTestCasesPushbutton_clicked() { - _test->createTestRailTestCases(); + _testCreator->createTestRailTestCases(); } void Nitpick::on_createTestRailRunButton_clicked() { - _test->createTestRailRun(); + _testCreator->createTestRailRun(); } void Nitpick::on_setWorkingFolderRunOnDesktopPushbutton_clicked() { @@ -206,16 +204,25 @@ void Nitpick::on_runNowPushbutton_clicked() { _testRunnerDesktop->run(); } +void Nitpick::on_usePreviousInstallationOnDesktopCheckBox_clicked() { + _ui.runLatestOnDesktopCheckBox->setEnabled(!_ui.usePreviousInstallationOnDesktopCheckBox->isChecked()); + _ui.urlOnDesktopLineEdit->setEnabled(!_ui.usePreviousInstallationOnDesktopCheckBox->isChecked() && !_ui.runLatestOnDesktopCheckBox->isChecked()); +} + void Nitpick::on_runLatestOnDesktopCheckBox_clicked() { _ui.urlOnDesktopLineEdit->setEnabled(!_ui.runLatestOnDesktopCheckBox->isChecked()); } +void Nitpick::on_runFullSuiteOnDesktopCheckBox_clicked() { + _ui.scriptURLOnDesktopLineEdit->setEnabled(!_ui.runFullSuiteOnDesktopCheckBox->isChecked()); +} + void Nitpick::automaticTestRunEvaluationComplete(QString zippedFolderName, int numberOfFailures) { _testRunnerDesktop->automaticTestRunEvaluationComplete(zippedFolderName, numberOfFailures); } void Nitpick::on_updateTestRailRunResultsPushbutton_clicked() { - _test->updateTestRailRunResult(); + _testCreator->updateTestRailRunResult(); } // To toggle between show and hide @@ -242,85 +249,24 @@ void Nitpick::on_showTaskbarPushbutton_clicked() { #endif } +void Nitpick::on_evaluateTestsPushbutton_clicked() { + _testCreator->startTestsEvaluation(false, false); +} + void Nitpick::on_closePushbutton_clicked() { exit(0); } void Nitpick::on_createPythonScriptRadioButton_clicked() { - _test->setTestRailCreateMode(PYTHON); + _testCreator->setTestRailCreateMode(PYTHON); } void Nitpick::on_createXMLScriptRadioButton_clicked() { - _test->setTestRailCreateMode(XML); + _testCreator->setTestRailCreateMode(XML); } void Nitpick::on_createWebPagePushbutton_clicked() { - _test->createWebPage(_ui.updateAWSCheckBox, _ui.awsURLLineEdit); -} - -void Nitpick::downloadFile(const QUrl& url) { - _downloaders.emplace_back(new Downloader(url, this)); - connect(_downloaders[_index], SIGNAL(downloaded()), _signalMapper, SLOT(map())); - - _signalMapper->setMapping(_downloaders[_index], _index); - - ++_index; -} - -void Nitpick::downloadFiles(const QStringList& URLs, const QString& directoryName, const QStringList& filenames, void *caller) { - connect(_signalMapper, SIGNAL(mapped(int)), this, SLOT(saveFile(int))); - - _directoryName = directoryName; - _filenames = filenames; - _caller = caller; - - _numberOfFilesToDownload = URLs.size(); - _numberOfFilesDownloaded = 0; - _index = 0; - - _ui.progressBar->setMinimum(0); - _ui.progressBar->setMaximum(_numberOfFilesToDownload - 1); - _ui.progressBar->setValue(0); - _ui.progressBar->setVisible(true); - - foreach (auto downloader, _downloaders) { - delete downloader; - } - - _downloaders.clear(); - for (int i = 0; i < _numberOfFilesToDownload; ++i) { - downloadFile(URLs[i]); - } -} - -void Nitpick::saveFile(int index) { - try { - QFile file(_directoryName + "/" + _filenames[index]); - file.open(QIODevice::WriteOnly); - file.write(_downloaders[index]->downloadedData()); - file.close(); - } catch (...) { - QMessageBox::information(0, "Test Aborted", "Failed to save file: " + _filenames[index]); - _ui.progressBar->setVisible(false); - return; - } - - ++_numberOfFilesDownloaded; - - if (_numberOfFilesDownloaded == _numberOfFilesToDownload) { - disconnect(_signalMapper, SIGNAL(mapped(int)), this, SLOT(saveFile(int))); - if (_caller == _test) { - _test->finishTestsEvaluation(); - } else if (_caller == _testRunnerDesktop) { - _testRunnerDesktop->downloadComplete(); - } else if (_caller == _testRunnerMobile) { - _testRunnerMobile->downloadComplete(); - } - - _ui.progressBar->setVisible(false); - } else { - _ui.progressBar->setValue(_numberOfFilesDownloaded); - } + _testCreator->createWebPage(_ui.updateAWSCheckBox, _ui.diffImageRadioButton, _ui.ssimImageRadioButton, _ui.awsURLLineEdit); } void Nitpick::about() { @@ -360,10 +306,19 @@ void Nitpick::on_connectDevicePushbutton_clicked() { _testRunnerMobile->connectDevice(); } +void Nitpick::on_usePreviousInstallationOnMobileCheckBox_clicked() { + _ui.runLatestOnMobileCheckBox->setEnabled(!_ui.usePreviousInstallationOnMobileCheckBox->isChecked()); + _ui.urlOnMobileLineEdit->setEnabled(!_ui.usePreviousInstallationOnMobileCheckBox->isChecked() && !_ui.runLatestOnMobileCheckBox->isChecked()); +} + void Nitpick::on_runLatestOnMobileCheckBox_clicked() { _ui.urlOnMobileLineEdit->setEnabled(!_ui.runLatestOnMobileCheckBox->isChecked()); } +void Nitpick::on_runFullSuiteOnMobileCheckBox_clicked() { + _ui.scriptURLOnMobileLineEdit->setEnabled(!_ui.runFullSuiteOnMobileCheckBox->isChecked()); +} + void Nitpick::on_downloadAPKPushbutton_clicked() { _testRunnerMobile->downloadAPK(); } diff --git a/tools/nitpick/src/Nitpick.h b/tools/nitpick/src/Nitpick.h index 80fef934d6..42f55ee8b2 100644 --- a/tools/nitpick/src/Nitpick.h +++ b/tools/nitpick/src/Nitpick.h @@ -11,12 +11,10 @@ #define hifi_Nitpick_h #include -#include #include #include "ui_Nitpick.h" -#include "Downloader.h" -#include "Test.h" +#include "TestCreator.h" #include "TestRunnerDesktop.h" #include "TestRunnerMobile.h" @@ -38,9 +36,6 @@ public: void automaticTestRunEvaluationComplete(QString zippedFolderName, int numberOfFailures); - void downloadFile(const QUrl& url); - void downloadFiles(const QStringList& URLs, const QString& directoryName, const QStringList& filenames, void* caller); - void setUserText(const QString& user); QString getSelectedUser(); @@ -56,7 +51,6 @@ private slots: void on_tabWidget_currentChanged(int index); - void on_evaluateTestsPushbutton_clicked(); void on_createRecursiveScriptPushbutton_clicked(); void on_createAllRecursiveScriptsPushbutton_clicked(); void on_createTestsPushbutton_clicked(); @@ -75,27 +69,32 @@ private slots: void on_setWorkingFolderRunOnDesktopPushbutton_clicked(); void on_runNowPushbutton_clicked(); + void on_usePreviousInstallationOnDesktopCheckBox_clicked(); void on_runLatestOnDesktopCheckBox_clicked(); + void on_runFullSuiteOnDesktopCheckBox_clicked(); void on_updateTestRailRunResultsPushbutton_clicked(); void on_hideTaskbarPushbutton_clicked(); void on_showTaskbarPushbutton_clicked(); + void on_evaluateTestsPushbutton_clicked(); + void on_createPythonScriptRadioButton_clicked(); void on_createXMLScriptRadioButton_clicked(); void on_createWebPagePushbutton_clicked(); - void saveFile(int index); - void about(); void content(); // Run on Mobile controls void on_setWorkingFolderRunOnMobilePushbutton_clicked(); void on_connectDevicePushbutton_clicked(); + + void on_usePreviousInstallationOnMobileCheckBox_clicked(); void on_runLatestOnMobileCheckBox_clicked(); + void on_runFullSuiteOnMobileCheckBox_clicked(); void on_downloadAPKPushbutton_clicked(); void on_installAPKPushbutton_clicked(); @@ -105,28 +104,13 @@ private slots: private: Ui::NitpickClass _ui; - Test* _test{ nullptr }; + TestCreator* _testCreator{ nullptr }; TestRunnerDesktop* _testRunnerDesktop{ nullptr }; TestRunnerMobile* _testRunnerMobile{ nullptr }; - std::vector _downloaders; - - // local storage for parameters - folder to store downloaded files in, and a list of their names - QString _directoryName; - QStringList _filenames; - - // Used to enable passing a parameter to slots - QSignalMapper* _signalMapper; - - int _numberOfFilesToDownload{ 0 }; - int _numberOfFilesDownloaded{ 0 }; - int _index{ 0 }; - bool _isRunningFromCommandline{ false }; - void* _caller; - QStringList clientProfiles; }; diff --git a/tools/nitpick/src/Test.cpp b/tools/nitpick/src/TestCreator.cpp similarity index 89% rename from tools/nitpick/src/Test.cpp rename to tools/nitpick/src/TestCreator.cpp index e8e284bf32..089e84904a 100644 --- a/tools/nitpick/src/Test.cpp +++ b/tools/nitpick/src/TestCreator.cpp @@ -1,5 +1,5 @@ // -// Test.cpp +// TestCreator.cpp // // Created by Nissim Hadar on 2 Nov 2017. // Copyright 2013 High Fidelity, Inc. @@ -7,7 +7,7 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "Test.h" +#include "TestCreator.h" #include #include @@ -24,7 +24,9 @@ extern Nitpick* nitpick; #include -Test::Test(QProgressBar* progressBar, QCheckBox* checkBoxInteractiveMode) : _awsInterface(NULL) { +TestCreator::TestCreator(QProgressBar* progressBar, QCheckBox* checkBoxInteractiveMode) : _awsInterface(NULL) { + _downloader = new Downloader(); + _progressBar = progressBar; _checkBoxInteractiveMode = checkBoxInteractiveMode; @@ -36,7 +38,7 @@ Test::Test(QProgressBar* progressBar, QCheckBox* checkBoxInteractiveMode) : _aws } } -bool Test::createTestResultsFolderPath(const QString& directory) { +bool TestCreator::createTestResultsFolderPath(const QString& directory) { QDateTime now = QDateTime::currentDateTime(); _testResultsFolderPath = directory + "/" + TEST_RESULTS_FOLDER + "--" + now.toString(DATETIME_FORMAT) + "(local)[" + QHostInfo::localHostName() + "]"; QDir testResultsFolder(_testResultsFolderPath); @@ -45,7 +47,7 @@ bool Test::createTestResultsFolderPath(const QString& directory) { return QDir().mkdir(_testResultsFolderPath); } -QString Test::zipAndDeleteTestResultsFolder() { +QString TestCreator::zipAndDeleteTestResultsFolder() { QString zippedResultsFileName { _testResultsFolderPath + ".zip" }; QFileInfo fileInfo(zippedResultsFileName); if (fileInfo.exists()) { @@ -65,7 +67,7 @@ QString Test::zipAndDeleteTestResultsFolder() { return zippedResultsFileName; } -int Test::compareImageLists() { +int TestCreator::compareImageLists() { _progressBar->setMinimum(0); _progressBar->setMaximum(_expectedImagesFullFilenames.length() - 1); _progressBar->setValue(0); @@ -89,23 +91,25 @@ int Test::compareImageLists() { QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Images are not the same size"); similarityIndex = -100.0; } else { - similarityIndex = _imageComparer.compareImages(resultImage, expectedImage); + _imageComparer.compareImages(resultImage, expectedImage); + similarityIndex = _imageComparer.getSSIMValue(); } TestResult testResult = TestResult{ (float)similarityIndex, _expectedImagesFullFilenames[i].left(_expectedImagesFullFilenames[i].lastIndexOf("/") + 1), // path to the test (including trailing /) QFileInfo(_expectedImagesFullFilenames[i].toStdString().c_str()).fileName(), // filename of expected image - QFileInfo(_resultImagesFullFilenames[i].toStdString().c_str()).fileName() // filename of result image + QFileInfo(_resultImagesFullFilenames[i].toStdString().c_str()).fileName(), // filename of result image + _imageComparer.getSSIMResults() // results of SSIM algoritm }; _mismatchWindow.setTestResult(testResult); - + if (similarityIndex < THRESHOLD) { ++numberOfFailures; if (!isInteractiveMode) { - appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), true); + appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), _mismatchWindow.getSSIMResultsImage(testResult._ssimResults), true); } else { _mismatchWindow.exec(); @@ -113,7 +117,7 @@ int Test::compareImageLists() { case USER_RESPONSE_PASS: break; case USE_RESPONSE_FAIL: - appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), true); + appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), _mismatchWindow.getSSIMResultsImage(testResult._ssimResults), true); break; case USER_RESPONSE_ABORT: keepOn = false; @@ -124,7 +128,7 @@ int Test::compareImageLists() { } } } else { - appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), false); + appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), _mismatchWindow.getSSIMResultsImage(testResult._ssimResults), false); } _progressBar->setValue(i); @@ -134,7 +138,7 @@ int Test::compareImageLists() { return numberOfFailures; } -int Test::checkTextResults() { +int TestCreator::checkTextResults() { // Create lists of failed and passed tests QStringList nameFilterFailed; nameFilterFailed << "*.failed.txt"; @@ -144,7 +148,7 @@ int Test::checkTextResults() { nameFilterPassed << "*.passed.txt"; QStringList testsPassed = QDir(_snapshotDirectory).entryList(nameFilterPassed, QDir::Files, QDir::Name); - // Add results to Test Results folder + // Add results to TestCreator Results folder foreach(QString currentFilename, testsFailed) { appendTestResultsToFile(currentFilename, true); } @@ -156,7 +160,7 @@ int Test::checkTextResults() { return testsFailed.length(); } -void Test::appendTestResultsToFile(TestResult testResult, QPixmap comparisonImage, bool hasFailed) { +void TestCreator::appendTestResultsToFile(const TestResult& testResult, const QPixmap& comparisonImage, const QPixmap& ssimResultsImage, bool hasFailed) { // Critical error if Test Results folder does not exist if (!QDir().exists(_testResultsFolderPath)) { QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Folder " + _testResultsFolderPath + " not found"); @@ -191,7 +195,7 @@ void Test::appendTestResultsToFile(TestResult testResult, QPixmap comparisonImag // Create text file describing the failure QTextStream stream(&descriptionFile); - stream << "Test in folder " << testResult._pathname.left(testResult._pathname.length() - 1) << endl; // remove trailing '/' + stream << "TestCreator in folder " << testResult._pathname.left(testResult._pathname.length() - 1) << endl; // remove trailing '/' stream << "Expected image was " << testResult._expectedImageFilename << endl; stream << "Actual image was " << testResult._actualImageFilename << endl; stream << "Similarity index was " << testResult._error << endl; @@ -217,9 +221,12 @@ void Test::appendTestResultsToFile(TestResult testResult, QPixmap comparisonImag } comparisonImage.save(resultFolderPath + "/" + "Difference Image.png"); + + // Save the SSIM results image + ssimResultsImage.save(resultFolderPath + "/" + "SSIM Image.png"); } -void::Test::appendTestResultsToFile(QString testResultFilename, bool hasFailed) { +void::TestCreator::appendTestResultsToFile(QString testResultFilename, bool hasFailed) { // The test name includes everything until the penultimate period QString testNameTemp = testResultFilename.left(testResultFilename.lastIndexOf('.')); QString testName = testResultFilename.left(testNameTemp.lastIndexOf('.')); @@ -246,7 +253,7 @@ void::Test::appendTestResultsToFile(QString testResultFilename, bool hasFailed) } } -void Test::startTestsEvaluation(const bool isRunningFromCommandLine, +void TestCreator::startTestsEvaluation(const bool isRunningFromCommandLine, const bool isRunningInAutomaticTestRun, const QString& snapshotDirectory, const QString& branchFromCommandLine, @@ -319,10 +326,11 @@ void Test::startTestsEvaluation(const bool isRunningFromCommandLine, } } - nitpick->downloadFiles(expectedImagesURLs, _snapshotDirectory, _expectedImagesFilenames, (void *)this); + _downloader->downloadFiles(expectedImagesURLs, _snapshotDirectory, _expectedImagesFilenames, (void *)this); + finishTestsEvaluation(); } -void Test::finishTestsEvaluation() { +void TestCreator::finishTestsEvaluation() { // First - compare the pairs of images int numberOfFailures = compareImageLists(); @@ -332,8 +340,10 @@ void Test::finishTestsEvaluation() { if (!_isRunningFromCommandLine && !_isRunningInAutomaticTestRun) { if (numberOfFailures == 0) { QMessageBox::information(0, "Success", "All images are as expected"); + } else if (numberOfFailures == 1) { + QMessageBox::information(0, "Failure", "One image is not as expected"); } else { - QMessageBox::information(0, "Failure", "One or more images are not as expected"); + QMessageBox::information(0, "Failure", QString::number(numberOfFailures) + " images are not as expected"); } } @@ -348,7 +358,7 @@ void Test::finishTestsEvaluation() { } } -bool Test::isAValidDirectory(const QString& pathname) { +bool TestCreator::isAValidDirectory(const QString& pathname) { // Only process directories QDir dir(pathname); if (!dir.exists()) { @@ -363,7 +373,7 @@ bool Test::isAValidDirectory(const QString& pathname) { return true; } -QString Test::extractPathFromTestsDown(const QString& fullPath) { +QString TestCreator::extractPathFromTestsDown(const QString& fullPath) { // `fullPath` includes the full path to the test. We need the portion below (and including) `tests` QStringList pathParts = fullPath.split('/'); int i{ 0 }; @@ -384,14 +394,14 @@ QString Test::extractPathFromTestsDown(const QString& fullPath) { return partialPath; } -void Test::includeTest(QTextStream& textStream, const QString& testPathname) { +void TestCreator::includeTest(QTextStream& textStream, const QString& testPathname) { QString partialPath = extractPathFromTestsDown(testPathname); QString partialPathWithoutTests = partialPath.right(partialPath.length() - 7); textStream << "Script.include(testsRootPath + \"" << partialPathWithoutTests + "\");" << endl; } -void Test::createTests(const QString& clientProfile) { +void TestCreator::createTests(const QString& clientProfile) { // Rename files sequentially, as ExpectedResult_00000.png, ExpectedResult_00001.png and so on // Any existing expected result images will be deleted QString previousSelection = _snapshotDirectory; @@ -469,7 +479,7 @@ void Test::createTests(const QString& clientProfile) { QMessageBox::information(0, "Success", "Test images have been created"); } -ExtractedText Test::getTestScriptLines(QString testFileName) { +ExtractedText TestCreator::getTestScriptLines(QString testFileName) { ExtractedText relevantTextFromTest; QFile inputFile(testFileName); @@ -534,7 +544,7 @@ ExtractedText Test::getTestScriptLines(QString testFileName) { return relevantTextFromTest; } -bool Test::createFileSetup() { +bool TestCreator::createFileSetup() { // Folder selection QString previousSelection = _testDirectory; QString parent = previousSelection.left(previousSelection.lastIndexOf('/')); @@ -554,7 +564,7 @@ bool Test::createFileSetup() { return true; } -bool Test::createAllFilesSetup() { +bool TestCreator::createAllFilesSetup() { // Select folder to start recursing from QString previousSelection = _testsRootDirectory; QString parent = previousSelection.left(previousSelection.lastIndexOf('/')); @@ -576,7 +586,7 @@ bool Test::createAllFilesSetup() { // Create an MD file for a user-selected test. // The folder selected must contain a script named "test.js", the file produced is named "test.md" -void Test::createMDFile() { +void TestCreator::createMDFile() { if (!createFileSetup()) { return; } @@ -586,7 +596,7 @@ void Test::createMDFile() { } } -void Test::createAllMDFiles() { +void TestCreator::createAllMDFiles() { if (!createAllFilesSetup()) { return; } @@ -618,7 +628,7 @@ void Test::createAllMDFiles() { QMessageBox::information(0, "Success", "MD files have been created"); } -bool Test::createMDFile(const QString& directory) { +bool TestCreator::createMDFile(const QString& directory) { // Verify folder contains test.js file QString testFileName(directory + "/" + TEST_FILENAME); QFileInfo testFileInfo(testFileName); @@ -638,7 +648,7 @@ bool Test::createMDFile(const QString& directory) { QTextStream stream(&mdFile); - //Test title + //TestCreator title QString testName = testScriptLines.title; stream << "# " << testName << "\n"; @@ -670,7 +680,7 @@ bool Test::createMDFile(const QString& directory) { return true; } -void Test::createTestAutoScript() { +void TestCreator::createTestAutoScript() { if (!createFileSetup()) { return; } @@ -680,7 +690,7 @@ void Test::createTestAutoScript() { } } -void Test::createAllTestAutoScripts() { +void TestCreator::createAllTestAutoScripts() { if (!createAllFilesSetup()) { return; } @@ -712,7 +722,7 @@ void Test::createAllTestAutoScripts() { QMessageBox::information(0, "Success", "All 'testAuto.js' scripts have been created"); } -bool Test::createTestAutoScript(const QString& directory) { +bool TestCreator::createTestAutoScript(const QString& directory) { // Verify folder contains test.js file QString testFileName(directory + "/" + TEST_FILENAME); QFileInfo testFileInfo(testFileName); @@ -743,7 +753,7 @@ bool Test::createTestAutoScript(const QString& directory) { // Creates a single script in a user-selected folder. // This script will run all text.js scripts in every applicable sub-folder -void Test::createRecursiveScript() { +void TestCreator::createRecursiveScript() { if (!createFileSetup()) { return; } @@ -753,7 +763,7 @@ void Test::createRecursiveScript() { } // This method creates a `testRecursive.js` script in every sub-folder. -void Test::createAllRecursiveScripts() { +void TestCreator::createAllRecursiveScripts() { if (!createAllFilesSetup()) { return; } @@ -763,7 +773,7 @@ void Test::createAllRecursiveScripts() { QMessageBox::information(0, "Success", "Scripts have been created"); } -void Test::createAllRecursiveScripts(const QString& directory) { +void TestCreator::createAllRecursiveScripts(const QString& directory) { QDirIterator it(directory, QDirIterator::Subdirectories); while (it.hasNext()) { @@ -775,7 +785,7 @@ void Test::createAllRecursiveScripts(const QString& directory) { } } -void Test::createRecursiveScript(const QString& directory, bool interactiveMode) { +void TestCreator::createRecursiveScript(const QString& directory, bool interactiveMode) { // If folder contains a test, then we are at a leaf const QString testPathname{ directory + "/" + TEST_FILENAME }; if (QFileInfo(testPathname).exists()) { @@ -841,7 +851,10 @@ void Test::createRecursiveScript(const QString& directory, bool interactiveMode) textStream << " nitpick = createNitpick(Script.resolvePath(\".\"));" << endl; textStream << " testsRootPath = nitpick.getTestsRootPath();" << endl << endl; textStream << " nitpick.enableRecursive();" << endl; - textStream << " nitpick.enableAuto();" << endl; + textStream << " nitpick.enableAuto();" << endl << endl; + textStream << " if (typeof Test !== 'undefined') {" << endl; + textStream << " Test.wait(10000);" << endl; + textStream << " }" << endl; textStream << "} else {" << endl; textStream << " depth++" << endl; textStream << "}" << endl << endl; @@ -861,7 +874,7 @@ void Test::createRecursiveScript(const QString& directory, bool interactiveMode) recursiveTestsFile.close(); } -void Test::createTestsOutline() { +void TestCreator::createTestsOutline() { QString previousSelection = _testDirectory; QString parent = previousSelection.left(previousSelection.lastIndexOf('/')); if (!parent.isNull() && parent.right(1) != "/") { @@ -887,7 +900,7 @@ void Test::createTestsOutline() { QTextStream stream(&mdFile); - //Test title + //TestCreator title stream << "# Outline of all tests\n"; stream << "Directories with an appended (*) have an automatic test\n\n"; @@ -945,10 +958,10 @@ void Test::createTestsOutline() { mdFile.close(); - QMessageBox::information(0, "Success", "Test outline file " + testsOutlineFilename + " has been created"); + QMessageBox::information(0, "Success", "TestCreator outline file " + testsOutlineFilename + " has been created"); } -void Test::createTestRailTestCases() { +void TestCreator::createTestRailTestCases() { QString previousSelection = _testDirectory; QString parent = previousSelection.left(previousSelection.lastIndexOf('/')); if (!parent.isNull() && parent.right(1) != "/") { @@ -985,7 +998,7 @@ void Test::createTestRailTestCases() { } } -void Test::createTestRailRun() { +void TestCreator::createTestRailRun() { QString outputDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select a folder to store generated files in", nullptr, QFileDialog::ShowDirsOnly); @@ -1001,9 +1014,9 @@ void Test::createTestRailRun() { _testRailInterface->createTestRailRun(outputDirectory); } -void Test::updateTestRailRunResult() { +void TestCreator::updateTestRailRunResult() { QString testResults = QFileDialog::getOpenFileName(nullptr, "Please select the zipped test results to update from", nullptr, - "Zipped Test Results (*.zip)"); + "Zipped TestCreator Results (*.zip)"); if (testResults.isNull()) { return; } @@ -1022,7 +1035,7 @@ void Test::updateTestRailRunResult() { _testRailInterface->updateTestRailRunResults(testResults, tempDirectory); } -QStringList Test::createListOfAll_imagesInDirectory(const QString& imageFormat, const QString& pathToImageDirectory) { +QStringList TestCreator::createListOfAll_imagesInDirectory(const QString& imageFormat, const QString& pathToImageDirectory) { _imageDirectory = QDir(pathToImageDirectory); QStringList nameFilters; nameFilters << "*." + imageFormat; @@ -1034,7 +1047,7 @@ QStringList Test::createListOfAll_imagesInDirectory(const QString& imageFormat, // Filename (i.e. without extension) contains tests (this is based on all test scripts being within the tests folder) // Last 5 characters in filename are digits (after removing the extension) // Extension is 'imageFormat' -bool Test::isInSnapshotFilenameFormat(const QString& imageFormat, const QString& filename) { +bool TestCreator::isInSnapshotFilenameFormat(const QString& imageFormat, const QString& filename) { bool contains_tests = filename.contains("tests" + PATH_SEPARATOR); QString filenameWithoutExtension = filename.left(filename.lastIndexOf('.')); @@ -1049,7 +1062,7 @@ bool Test::isInSnapshotFilenameFormat(const QString& imageFormat, const QString& // For a file named "D_GitHub_hifi-tests_tests_content_entity_zone_create_0.jpg", the test directory is // D:/GitHub/hifi-tests/tests/content/entity/zone/create // This method assumes the filename is in the correct format -QString Test::getExpectedImageDestinationDirectory(const QString& filename) { +QString TestCreator::getExpectedImageDestinationDirectory(const QString& filename) { QString filenameWithoutExtension = filename.left(filename.length() - 4); QStringList filenameParts = filenameWithoutExtension.split(PATH_SEPARATOR); @@ -1066,7 +1079,7 @@ QString Test::getExpectedImageDestinationDirectory(const QString& filename) { // is ...tests/content/entity/zone/create // This is used to create the full URL // This method assumes the filename is in the correct format -QString Test::getExpectedImagePartialSourceDirectory(const QString& filename) { +QString TestCreator::getExpectedImagePartialSourceDirectory(const QString& filename) { QString filenameWithoutExtension = filename.left(filename.length() - 4); QStringList filenameParts = filenameWithoutExtension.split(PATH_SEPARATOR); @@ -1091,13 +1104,18 @@ QString Test::getExpectedImagePartialSourceDirectory(const QString& filename) { return result; } -void Test::setTestRailCreateMode(TestRailCreateMode testRailCreateMode) { +void TestCreator::setTestRailCreateMode(TestRailCreateMode testRailCreateMode) { _testRailCreateMode = testRailCreateMode; } -void Test::createWebPage(QCheckBox* updateAWSCheckBox, QLineEdit* urlLineEdit) { +void TestCreator::createWebPage( + QCheckBox* updateAWSCheckBox, + QRadioButton* diffImageRadioButton, + QRadioButton* ssimImageRadionButton, + QLineEdit* urlLineEdit +) { QString testResults = QFileDialog::getOpenFileName(nullptr, "Please select the zipped test results to update from", nullptr, - "Zipped Test Results (TestResults--*.zip)"); + "Zipped TestCreator Results (TestResults--*.zip)"); if (testResults.isNull()) { return; } @@ -1112,5 +1130,12 @@ void Test::createWebPage(QCheckBox* updateAWSCheckBox, QLineEdit* urlLineEdit) { _awsInterface = new AWSInterface; } - _awsInterface->createWebPageFromResults(testResults, workingDirectory, updateAWSCheckBox, urlLineEdit); + _awsInterface->createWebPageFromResults( + testResults, + workingDirectory, + updateAWSCheckBox, + diffImageRadioButton, + ssimImageRadionButton, + urlLineEdit + ); } \ No newline at end of file diff --git a/tools/nitpick/src/Test.h b/tools/nitpick/src/TestCreator.h similarity index 88% rename from tools/nitpick/src/Test.h rename to tools/nitpick/src/TestCreator.h index 23011d0c31..7cd38b42d4 100644 --- a/tools/nitpick/src/Test.h +++ b/tools/nitpick/src/TestCreator.h @@ -1,5 +1,5 @@ // -// Test.h +// TestCreator.h // // Created by Nissim Hadar on 2 Nov 2017. // Copyright 2013 High Fidelity, Inc. @@ -8,8 +8,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#ifndef hifi_test_h -#define hifi_test_h +#ifndef hifi_testCreator_h +#define hifi_testCreator_h #include #include @@ -18,6 +18,7 @@ #include "AWSInterface.h" #include "ImageComparer.h" +#include "Downloader.h" #include "MismatchWindow.h" #include "TestRailInterface.h" @@ -40,9 +41,9 @@ enum TestRailCreateMode { XML }; -class Test { +class TestCreator { public: - Test(QProgressBar* progressBar, QCheckBox* checkBoxInteractiveMode); + TestCreator(QProgressBar* progressBar, QCheckBox* checkBoxInteractiveMode); void startTestsEvaluation(const bool isRunningFromCommandLine, const bool isRunningInAutomaticTestRun, @@ -87,7 +88,7 @@ public: void includeTest(QTextStream& textStream, const QString& testPathname); - void appendTestResultsToFile(TestResult testResult, QPixmap comparisonImage, bool hasFailed); + void appendTestResultsToFile(const TestResult& testResult, const QPixmap& comparisonImage, const QPixmap& ssimResultsImage, bool hasFailed); void appendTestResultsToFile(QString testResultFilename, bool hasFailed); bool createTestResultsFolderPath(const QString& directory); @@ -102,7 +103,11 @@ public: void setTestRailCreateMode(TestRailCreateMode testRailCreateMode); - void createWebPage(QCheckBox* updateAWSCheckBox, QLineEdit* urlLineEdit); + void createWebPage( + QCheckBox* updateAWSCheckBox, + QRadioButton* diffImageRadioButton, + QRadioButton* ssimImageRadionButton, + QLineEdit* urlLineEdit); private: QProgressBar* _progressBar; @@ -116,7 +121,7 @@ private: const QString TEST_RESULTS_FOLDER { "TestResults" }; const QString TEST_RESULTS_FILENAME { "TestResults.txt" }; - const double THRESHOLD{ 0.935 }; + const double THRESHOLD{ 0.9999 }; QDir _imageDirectory; @@ -163,6 +168,7 @@ private: TestRailCreateMode _testRailCreateMode { PYTHON }; AWSInterface* _awsInterface; + Downloader* _downloader; }; -#endif // hifi_test_h \ No newline at end of file +#endif \ No newline at end of file diff --git a/tools/nitpick/src/TestRailInterface.cpp b/tools/nitpick/src/TestRailInterface.cpp index 6ed13a72b6..d2303473ed 100644 --- a/tools/nitpick/src/TestRailInterface.cpp +++ b/tools/nitpick/src/TestRailInterface.cpp @@ -9,7 +9,7 @@ // #include "TestRailInterface.h" -#include "Test.h" +#include "TestCreator.h" #include #include @@ -258,7 +258,7 @@ bool TestRailInterface::requestTestRailResultsDataFromUser() { } bool TestRailInterface::isAValidTestDirectory(const QString& directory) { - if (Test::isAValidDirectory(directory)) { + if (TestCreator::isAValidDirectory(directory)) { // Ignore the utils and preformance directories if (directory.right(QString("utils").length()) == "utils" || directory.right(QString("performance").length()) == "performance") { @@ -352,6 +352,7 @@ void TestRailInterface::createAddTestCasesPythonScript(const QString& testDirect ) { QProcess* process = new QProcess(); + _busyWindow.setWindowTitle("Updating TestRail"); connect(process, &QProcess::started, this, [=]() { _busyWindow.exec(); }); connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater())); connect(process, static_cast(&QProcess::finished), this, @@ -482,6 +483,7 @@ void TestRailInterface::addRun() { QMessageBox::Yes | QMessageBox::No).exec() ) { QProcess* process = new QProcess(); + _busyWindow.setWindowTitle("Updating TestRail"); connect(process, &QProcess::started, this, [=]() { _busyWindow.exec(); }); connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater())); connect(process, static_cast(&QProcess::finished), this, @@ -591,6 +593,7 @@ void TestRailInterface::updateRunWithResults() { QMessageBox::Yes | QMessageBox::No).exec() ) { QProcess* process = new QProcess(); + _busyWindow.setWindowTitle("Updating TestRail"); connect(process, &QProcess::started, this, [=]() { _busyWindow.exec(); }); connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater())); connect(process, static_cast(&QProcess::finished), this, diff --git a/tools/nitpick/src/TestRunner.cpp b/tools/nitpick/src/TestRunner.cpp index 54246de80b..c4e991e5ee 100644 --- a/tools/nitpick/src/TestRunner.cpp +++ b/tools/nitpick/src/TestRunner.cpp @@ -14,6 +14,26 @@ #include "Nitpick.h" extern Nitpick* nitpick; +TestRunner::TestRunner( + QLabel* workingFolderLabel, + QLabel* statusLabel, + QCheckBox* usePreviousInstallationCheckBox, + QCheckBox* runLatest, + QLineEdit* url, + QCheckBox* runFullSuite, + QLineEdit* scriptURL +) { + _workingFolderLabel = workingFolderLabel; + _statusLabel = statusLabel; + _usePreviousInstallationCheckBox = usePreviousInstallationCheckBox; + _runLatest = runLatest; + _url = url; + _runFullSuite = runFullSuite; + _scriptURL = scriptURL; + + _downloader = new Downloader(); +} + void TestRunner::setWorkingFolder(QLabel* workingFolderLabel) { // Everything will be written to this folder QString previousSelection = _workingFolder; @@ -49,7 +69,7 @@ void TestRunner::downloadBuildXml(void* caller) { urls << DEV_BUILD_XML_URL; filenames << DEV_BUILD_XML_FILENAME; - nitpick->downloadFiles(urls, _workingFolder, filenames, caller); + _downloader->downloadFiles(urls, _workingFolder, filenames, caller); } void TestRunner::parseBuildInformation() { diff --git a/tools/nitpick/src/TestRunner.h b/tools/nitpick/src/TestRunner.h index d2468ec2fa..6d36f246f7 100644 --- a/tools/nitpick/src/TestRunner.h +++ b/tools/nitpick/src/TestRunner.h @@ -11,6 +11,8 @@ #ifndef hifi_testRunner_h #define hifi_testRunner_h +#include "Downloader.h" + #include #include #include @@ -28,7 +30,18 @@ public: class TestRunner { public: + TestRunner( + QLabel* workingFolderLabel, + QLabel* statusLabel, + QCheckBox* usePreviousInstallationOnMobileCheckBox, + QCheckBox* runLatest, + QLineEdit* url, + QCheckBox* runFullSuite, + QLineEdit* scriptURL + ); + void setWorkingFolder(QLabel* workingFolderLabel); + void downloadBuildXml(void* caller); void parseBuildInformation(); QString getInstallerNameFromURL(const QString& url); @@ -36,10 +49,15 @@ public: void appendLog(const QString& message); protected: + Downloader* _downloader; + QLabel* _workingFolderLabel; QLabel* _statusLabel; - QLineEdit* _url; + QCheckBox* _usePreviousInstallationCheckBox; QCheckBox* _runLatest; + QLineEdit* _url; + QCheckBox* _runFullSuite; + QLineEdit* _scriptURL; QString _workingFolder; diff --git a/tools/nitpick/src/TestRunnerDesktop.cpp b/tools/nitpick/src/TestRunnerDesktop.cpp index e45d895886..b9caaa0ecb 100644 --- a/tools/nitpick/src/TestRunnerDesktop.cpp +++ b/tools/nitpick/src/TestRunnerDesktop.cpp @@ -27,23 +27,22 @@ TestRunnerDesktop::TestRunnerDesktop( std::vector timeEdits, QLabel* workingFolderLabel, QCheckBox* runServerless, + QCheckBox* usePreviousInstallationOnMobileCheckBox, QCheckBox* runLatest, QLineEdit* url, + QCheckBox* runFullSuite, + QLineEdit* scriptURL, QPushButton* runNow, QLabel* statusLabel, QObject* parent -) : QObject(parent) +) : QObject(parent), TestRunner(workingFolderLabel, statusLabel, usePreviousInstallationOnMobileCheckBox, runLatest, url, runFullSuite, scriptURL) { _dayCheckboxes = dayCheckboxes; _timeEditCheckboxes = timeEditCheckboxes; _timeEdits = timeEdits; - _workingFolderLabel = workingFolderLabel; _runServerless = runServerless; - _runLatest = runLatest; - _url = url; _runNow = runNow; - _statusLabel = statusLabel; _installerThread = new QThread(); _installerWorker = new InstallerWorker(); @@ -179,10 +178,14 @@ void TestRunnerDesktop::run() { // This will be restored at the end of the tests saveExistingHighFidelityAppDataFolder(); - _statusLabel->setText("Downloading Build XML"); - downloadBuildXml((void*)this); + if (_usePreviousInstallationCheckBox->isChecked()) { + installationComplete(); + } else { + _statusLabel->setText("Downloading Build XML"); + downloadBuildXml((void*)this); - // `downloadComplete` will run after download has completed + downloadComplete(); + } } void TestRunnerDesktop::downloadComplete() { @@ -209,9 +212,9 @@ void TestRunnerDesktop::downloadComplete() { _statusLabel->setText("Downloading installer"); - nitpick->downloadFiles(urls, _workingFolder, filenames, (void*)this); + _downloader->downloadFiles(urls, _workingFolder, filenames, (void*)this); - // `downloadComplete` will run again after download has completed + downloadComplete(); } else { // Download of Installer has completed @@ -292,15 +295,19 @@ void TestRunnerDesktop::installationComplete() { void TestRunnerDesktop::verifyInstallationSucceeded() { // Exit if the executables are missing. - // On Windows, the reason is probably that UAC has blocked the installation. This is treated as a critical error #ifdef Q_OS_WIN QFileInfo interfaceExe(QDir::toNativeSeparators(_installationFolder) + "\\interface.exe"); QFileInfo assignmentClientExe(QDir::toNativeSeparators(_installationFolder) + "\\assignment-client.exe"); QFileInfo domainServerExe(QDir::toNativeSeparators(_installationFolder) + "\\domain-server.exe"); if (!interfaceExe.exists() || !assignmentClientExe.exists() || !domainServerExe.exists()) { - QMessageBox::critical(0, "Installation of High Fidelity has failed", "Please verify that UAC has been disabled"); - exit(-1); + if (_runLatest->isChecked()) { + // On Windows, the reason is probably that UAC has blocked the installation. This is treated as a critical error + QMessageBox::critical(0, "Installation of High Fidelity has failed", "Please verify that UAC has been disabled"); + exit(-1); + } else { + QMessageBox::critical(0, "Installation of High Fidelity not found", "Please verify that working folder contains a proper installation"); + } } #endif } @@ -457,8 +464,9 @@ void TestRunnerDesktop::runInterfaceWithTestScript() { QString deleteScript = QString("https://raw.githubusercontent.com/") + _user + "/hifi_tests/" + _branch + "/tests/utils/deleteNearbyEntities.js"; - QString testScript = - QString("https://raw.githubusercontent.com/") + _user + "/hifi_tests/" + _branch + "/tests/testRecursive.js"; + QString testScript = (_runFullSuite->isChecked()) + ? QString("https://raw.githubusercontent.com/") + _user + "/hifi_tests/" + _branch + "/tests/testRecursive.js" + : _scriptURL->text(); QString commandLine; #ifdef Q_OS_WIN @@ -537,15 +545,16 @@ void TestRunnerDesktop::runInterfaceWithTestScript() { } void TestRunnerDesktop::interfaceExecutionComplete() { + QThread::msleep(500); QFileInfo testCompleted(QDir::toNativeSeparators(_snapshotFolder) +"/tests_completed.txt"); if (!testCompleted.exists()) { QMessageBox::critical(0, "Tests not completed", "Interface seems to have crashed before completion of the test scripts\nExisting images will be evaluated"); } + killProcesses(); + evaluateResults(); - killProcesses(); - // The High Fidelity AppData folder will be restored after evaluation has completed } @@ -591,7 +600,6 @@ void TestRunnerDesktop::addBuildNumberToResults(const QString& zippedFolderName) if (!QFile::rename(zippedFolderName, augmentedFilename)) { QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Could not rename '" + zippedFolderName + "' to '" + augmentedFilename); exit(-1); - } } @@ -667,6 +675,13 @@ void TestRunnerDesktop::checkTime() { QString TestRunnerDesktop::getPRNumberFromURL(const QString& url) { try { QStringList urlParts = url.split("/"); + if (urlParts.size() <= 2) { +#ifdef Q_OS_WIN + throw "URL not in expected format, should look like `https://deployment.highfidelity.com/jobs/pr-build/label%3Dwindows/13023/HighFidelity-Beta-Interface-PR14006-be76c43.exe`"; +#elif defined Q_OS_MAC + throw "URL not in expected format, should look like `https://deployment.highfidelity.com/jobs/pr-build/label%3Dwindows/13023/HighFidelity-Beta-Interface-PR14006-be76c43.dmg`"; +#endif + } QStringList filenameParts = urlParts[urlParts.size() - 1].split("-"); if (filenameParts.size() <= 3) { #ifdef Q_OS_WIN diff --git a/tools/nitpick/src/TestRunnerDesktop.h b/tools/nitpick/src/TestRunnerDesktop.h index 140a81f465..dce2dce2ba 100644 --- a/tools/nitpick/src/TestRunnerDesktop.h +++ b/tools/nitpick/src/TestRunnerDesktop.h @@ -12,7 +12,6 @@ #define hifi_testRunnerDesktop_h #include -#include #include #include #include @@ -32,8 +31,11 @@ public: std::vector timeEdits, QLabel* workingFolderLabel, QCheckBox* runServerless, + QCheckBox* usePreviousInstallationOnMobileCheckBox, QCheckBox* runLatest, QLineEdit* url, + QCheckBox* runFullSuite, + QLineEdit* scriptURL, QPushButton* runNow, QLabel* statusLabel, @@ -99,7 +101,6 @@ private: std::vector _dayCheckboxes; std::vector _timeEditCheckboxes; std::vector _timeEdits; - QLabel* _workingFolderLabel; QCheckBox* _runServerless; QPushButton* _runNow; QTimer* _timer; diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index ab276f3337..4d0d18ef3d 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -25,14 +25,16 @@ TestRunnerMobile::TestRunnerMobile( QPushButton* downloadAPKPushbutton, QPushButton* installAPKPushbutton, QPushButton* runInterfacePushbutton, + QCheckBox* usePreviousInstallationOnMobileCheckBox, QCheckBox* runLatest, QLineEdit* url, + QCheckBox* runFullSuite, + QLineEdit* scriptURL, QLabel* statusLabel, QObject* parent -) : QObject(parent), _adbInterface(NULL) +) : QObject(parent), TestRunner(workingFolderLabel, statusLabel, usePreviousInstallationOnMobileCheckBox, runLatest, url, runFullSuite, scriptURL) { - _workingFolderLabel = workingFolderLabel; _connectDeviceButton = connectDeviceButton; _pullFolderButton = pullFolderButton; _detectedDeviceLabel = detectedDeviceLabel; @@ -40,13 +42,15 @@ TestRunnerMobile::TestRunnerMobile( _downloadAPKPushbutton = downloadAPKPushbutton; _installAPKPushbutton = installAPKPushbutton; _runInterfacePushbutton = runInterfacePushbutton; - _runLatest = runLatest; - _url = url; - _statusLabel = statusLabel; folderLineEdit->setText("/sdcard/DCIM/TEST"); modelNames["SM_G955U1"] = "Samsung S8+ unlocked"; + modelNames["SM_N960U1"] = "Samsung Note 9 unlocked"; + modelNames["SM_T380"] = "Samsung Tab A"; + modelNames["Quest"] = "Quest"; + + _adbInterface = NULL; } TestRunnerMobile::~TestRunnerMobile() { @@ -66,6 +70,7 @@ void TestRunnerMobile::connectDevice() { QString devicesFullFilename{ _workingFolder + "/devices.txt" }; QString command = _adbInterface->getAdbCommand() + " devices -l > " + devicesFullFilename; + appendLog(command); system(command.toStdString().c_str()); if (!QFile::exists(devicesFullFilename)) { @@ -93,7 +98,7 @@ void TestRunnerMobile::connectDevice() { QString deviceID = tokens[0]; QString modelID = tokens[3].split(':')[1]; - QString modelName = "UKNOWN"; + QString modelName = "UNKNOWN"; if (modelNames.count(modelID) == 1) { modelName = modelNames[modelID]; } @@ -102,6 +107,8 @@ void TestRunnerMobile::connectDevice() { _pullFolderButton->setEnabled(true); _folderLineEdit->setEnabled(true); _downloadAPKPushbutton->setEnabled(true); + _installAPKPushbutton->setEnabled(true); + _runInterfacePushbutton->setEnabled(true); } } #endif @@ -109,6 +116,8 @@ void TestRunnerMobile::connectDevice() { void TestRunnerMobile::downloadAPK() { downloadBuildXml((void*)this); + + downloadComplete(); } @@ -141,11 +150,12 @@ void TestRunnerMobile::downloadComplete() { _statusLabel->setText("Downloading installer"); - nitpick->downloadFiles(urls, _workingFolder, filenames, (void*)this); + _downloader->downloadFiles(urls, _workingFolder, filenames, (void*)this); } else { _statusLabel->setText("Installer download complete"); - _installAPKPushbutton->setEnabled(true); } + + _installAPKPushbutton->setEnabled(true); } void TestRunnerMobile::installAPK() { @@ -154,11 +164,25 @@ void TestRunnerMobile::installAPK() { _adbInterface = new AdbInterface(); } + if (_installerFilename.isNull()) { + QString installerPathname = QFileDialog::getOpenFileName(nullptr, "Please select the APK", _workingFolder, + "Available APKs (*.apk)" + ); + + if (installerPathname.isNull()) { + return; + } + + // Remove the path + QStringList parts = installerPathname.split('/'); + _installerFilename = parts[parts.length() - 1]; + } + _statusLabel->setText("Installing"); QString command = _adbInterface->getAdbCommand() + " install -r -d " + _workingFolder + "/" + _installerFilename + " >" + _workingFolder + "/installOutput.txt"; + appendLog(command); system(command.toStdString().c_str()); _statusLabel->setText("Installation complete"); - _runInterfacePushbutton->setEnabled(true); #endif } @@ -169,7 +193,22 @@ void TestRunnerMobile::runInterface() { } _statusLabel->setText("Starting Interface"); - QString command = _adbInterface->getAdbCommand() + " shell monkey -p io.highfidelity.hifiinterface -v 1"; + + QString testScript = (_runFullSuite->isChecked()) + ? QString("https://raw.githubusercontent.com/") + nitpick->getSelectedUser() + "/hifi_tests/" + nitpick->getSelectedBranch() + "/tests/testRecursive.js" + : _scriptURL->text(); + + QString command = _adbInterface->getAdbCommand() + + " shell am start -n io.highfidelity.hifiinterface/.PermissionChecker" + + " --es args \\\"" + + " --url file:///~/serverless/tutorial.json" + + " --no-updater" + + " --no-login-suggestion" + + " --testScript " + testScript + " quitWhenFinished" + + " --testResultsLocation /sdcard/snapshots" + + "\\\""; + + appendLog(command); system(command.toStdString().c_str()); _statusLabel->setText("Interface started"); #endif @@ -182,7 +221,8 @@ void TestRunnerMobile::pullFolder() { } _statusLabel->setText("Pulling folder"); - QString command = _adbInterface->getAdbCommand() + " pull " + _folderLineEdit->text() + " " + _workingFolder + _installerFilename; + QString command = _adbInterface->getAdbCommand() + " pull " + _folderLineEdit->text() + " " + _workingFolder; + appendLog(command); system(command.toStdString().c_str()); _statusLabel->setText("Pull complete"); #endif diff --git a/tools/nitpick/src/TestRunnerMobile.h b/tools/nitpick/src/TestRunnerMobile.h index 52c2ba096d..f7b16da6f8 100644 --- a/tools/nitpick/src/TestRunnerMobile.h +++ b/tools/nitpick/src/TestRunnerMobile.h @@ -12,7 +12,6 @@ #define hifi_testRunnerMobile_h #include -#include #include #include @@ -31,8 +30,11 @@ public: QPushButton* downloadAPKPushbutton, QPushButton* installAPKPushbutton, QPushButton* runInterfacePushbutton, + QCheckBox* usePreviousInstallationOnMobileCheckBox, QCheckBox* runLatest, QLineEdit* url, + QCheckBox* runFullSuite, + QLineEdit* scriptURL, QLabel* statusLabel, QObject* parent = 0 diff --git a/tools/nitpick/src/common.h b/tools/nitpick/src/common.h index 5df4e9c921..eb228ff2b3 100644 --- a/tools/nitpick/src/common.h +++ b/tools/nitpick/src/common.h @@ -10,21 +10,38 @@ #ifndef hifi_common_h #define hifi_common_h +#include #include +class SSIMResults { +public: + int width; + int height; + std::vector results; + double ssim; + + // Used for scaling + double min; + double max; +}; + class TestResult { public: - TestResult(float error, QString pathname, QString expectedImageFilename, QString actualImageFilename) : + TestResult(float error, const QString& pathname, const QString& expectedImageFilename, const QString& actualImageFilename, const SSIMResults& ssimResults) : _error(error), _pathname(pathname), _expectedImageFilename(expectedImageFilename), - _actualImageFilename(actualImageFilename) + _actualImageFilename(actualImageFilename), + _ssimResults(ssimResults) {} double _error; + QString _pathname; QString _expectedImageFilename; QString _actualImageFilename; + + SSIMResults _ssimResults; }; enum UserResponse { diff --git a/tools/nitpick/ui/Nitpick.ui b/tools/nitpick/ui/Nitpick.ui index 47471522db..a0f368863d 100644 --- a/tools/nitpick/ui/Nitpick.ui +++ b/tools/nitpick/ui/Nitpick.ui @@ -34,6 +34,9 @@ + + true + 45 @@ -43,7 +46,7 @@ - 0 + 5 @@ -495,7 +498,7 @@ - 20 + 240 70 120 20 @@ -549,13 +552,80 @@ - 170 + 175 100 - 451 + 445 21 + + + + 128 + 125 + 40 + 31 + + + + Script + + + + + false + + + + 175 + 130 + 445 + 21 + + + + + + + 20 + 130 + 120 + 20 + + + + <html><head/><body><p>If unchecked, will not show results during evaluation</p></body></html> + + + Run Full Suite + + + true + + + + + true + + + + 20 + 70 + 171 + 20 + + + + <html><head/><body><p>If unchecked, will not show results during evaluation</p></body></html> + + + usePreviousInstallation + + + false + + @@ -568,7 +638,7 @@ 10 - 90 + 150 160 30 @@ -581,7 +651,7 @@ 190 - 96 + 156 320 30 @@ -623,7 +693,7 @@ 460 - 410 + 440 160 30 @@ -639,7 +709,7 @@ 10 - 410 + 440 440 30 @@ -651,9 +721,9 @@ - 170 - 170 - 451 + 175 + 245 + 445 21 @@ -662,7 +732,7 @@ 20 - 170 + 245 120 20 @@ -684,7 +754,7 @@ 10 - 210 + 100 160 30 @@ -696,7 +766,7 @@ - 300 + 20 60 41 31 @@ -709,7 +779,7 @@ - 350 + 70 60 271 31 @@ -726,7 +796,7 @@ 10 - 250 + 325 160 30 @@ -742,7 +812,7 @@ 10 - 300 + 375 160 30 @@ -751,6 +821,86 @@ Run Interface + + + + 140 + 240 + 31 + 31 + + + + URL + + + + + false + + + + 175 + 275 + 445 + 21 + + + + + + + 140 + 270 + 40 + 31 + + + + Script + + + + + + 20 + 275 + 120 + 20 + + + + <html><head/><body><p>If unchecked, will not show results during evaluation</p></body></html> + + + Run Full Suite + + + true + + + + + true + + + + 20 + 210 + 171 + 20 + + + + <html><head/><body><p>If unchecked, will not show results during evaluation</p></body></html> + + + usePreviousInstallation + + + false + + @@ -760,7 +910,7 @@ 190 - 180 + 200 131 20 @@ -776,7 +926,7 @@ 330 - 170 + 190 181 51 @@ -889,8 +1039,8 @@ - 270 - 30 + 370 + 20 160 51 @@ -921,6 +1071,41 @@ 21 + + true + + + + + + 260 + 50 + 95 + 20 + + + + Diff Image + + + false + + + + + + 260 + 30 + 95 + 20 + + + + SSIM Image + + + true + groupBox diff --git a/tools/shadergen.py b/tools/shadergen.py index ffbe1662ec..f82b471f17 100644 --- a/tools/shadergen.py +++ b/tools/shadergen.py @@ -49,11 +49,36 @@ def getCommonScribeArgs(scribefile, includeLibs): scribeArgs.append(scribefile) return scribeArgs -def getDialectAndVariantHeaders(dialect, variant): +extensionsHeaderMutex = Lock() + +def getExtensionsHeader(dialect, variant, extensions): + extensionHeader = '{}/extensions_{}_{}.glsl'.format(args.build_dir, dialect, variant) + global extensionsHeaderMutex + extensionsHeaderMutex.acquire() + if not os.path.exists(extensionHeader): + extensionsDefines = [] + for extension in extensions: + extensionsDefines.append('#define HAVE_{}'.format(extension)) + # make sure we end with a line feed + extensionsDefines.append("\r\n") + with open(extensionHeader, "w") as f: + f.write('\r\n'.join(extensionsDefines)) + extensionsHeaderMutex.release() + return extensionHeader + + +def getDialectAndVariantHeaders(dialect, variant, extensions=None): + result = [] headerPath = args.source_dir + '/libraries/shaders/headers/' - variantHeader = headerPath + ('stereo.glsl' if (variant == 'stereo') else 'mono.glsl') + versionHeader = headerPath + dialect + '/version.glsl' + result.append(versionHeader) + if extensions is not None: + result.append(getExtensionsHeader(dialect, variant, extensions)) dialectHeader = headerPath + dialect + '/header.glsl' - return [dialectHeader, variantHeader] + result.append(dialectHeader) + variantHeader = headerPath + ('stereo.glsl' if (variant == 'stereo') else 'mono.glsl') + result.append(variantHeader) + return result class ScribeDependenciesCache: cache = {} @@ -170,7 +195,7 @@ def processCommand(line): scribeDepCache.gen(scribeFile, libs, dialect, variant) scribeArgs = getCommonScribeArgs(scribeFile, libs) - for header in getDialectAndVariantHeaders(dialect, variant): + for header in getDialectAndVariantHeaders(dialect, variant, args.extensions): scribeArgs.extend(['-H', header]) scribeArgs.extend(['-o', unoptGlslFile]) executeSubprocess(scribeArgs) @@ -218,6 +243,7 @@ def main(): parser = ArgumentParser(description='Generate shader artifacts.') +parser.add_argument('--extensions', type=str, nargs='*', help='Available extensions for the shaders') parser.add_argument('--commands', type=argparse.FileType('r'), help='list of commands to execute') parser.add_argument('--tools-dir', type=str, help='location of the host compatible binaries') parser.add_argument('--build-dir', type=str, help='The build directory base path') @@ -230,8 +256,8 @@ args = None if len(sys.argv) == 1: # for debugging sourceDir = expanduser('~/git/hifi') - toolsDir = os.path.join(expanduser('~/git/vcpkg'), 'installed', 'x64-windows', 'tools') - buildPath = sourceDir + '/build' + toolsDir = 'd:/hifi/vcpkg/android/fd82f0a8/installed/x64-windows/tools' + buildPath = sourceDir + '/build_android' commandsPath = buildPath + '/libraries/shaders/shadergen.txt' shaderDir = buildPath + '/libraries/shaders' testArgs = '--commands {} --tools-dir {} --build-dir {} --source-dir {}'.format( @@ -239,6 +265,7 @@ if len(sys.argv) == 1: ).split() testArgs.append('--debug') testArgs.append('--force') + testArgs.extend('--extensions EXT_clip_cull_distance'.split()) #testArgs.append('--dry-run') args = parser.parse_args(testArgs) else: diff --git a/tools/unity-avatar-exporter/Assets/Editor.meta b/tools/unity-avatar-exporter/Assets/Editor.meta deleted file mode 100644 index cf7dcf12dd..0000000000 --- a/tools/unity-avatar-exporter/Assets/Editor.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 02111c50e71dd664da8ad5c6a6eca767 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs index 7b90145223..8160ad8aa0 100644 --- a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs @@ -14,12 +14,14 @@ using System.Collections.Generic; class AvatarExporter : MonoBehaviour { // update version number for every PR that changes this file, also set updated version in README file - static readonly string AVATAR_EXPORTER_VERSION = "0.2"; + static readonly string AVATAR_EXPORTER_VERSION = "0.3.1"; static readonly float HIPS_GROUND_MIN_Y = 0.01f; static readonly float HIPS_SPINE_CHEST_MIN_SEPARATION = 0.001f; static readonly int MAXIMUM_USER_BONE_COUNT = 256; static readonly string EMPTY_WARNING_TEXT = "None"; + static readonly string TEXTURES_DIRECTORY = "textures"; + static readonly string DEFAULT_MATERIAL_NAME = "No Name"; // TODO: use regex static readonly string[] RECOMMENDED_UNITY_VERSIONS = new string[] { @@ -195,8 +197,17 @@ class AvatarExporter : MonoBehaviour { " Thumb Intermediate", " Thumb Proximal", }; + + static readonly string STANDARD_SHADER = "Standard"; + static readonly string STANDARD_ROUGHNESS_SHADER = "Standard (Roughness setup)"; + static readonly string STANDARD_SPECULAR_SHADER = "Standard (Specular setup)"; + static readonly string[] SUPPORTED_SHADERS = new string[] { + STANDARD_SHADER, + STANDARD_ROUGHNESS_SHADER, + STANDARD_SPECULAR_SHADER, + }; - enum BoneRule { + enum AvatarRule { RecommendedUnityVersion, SingleRoot, NoDuplicateMapping, @@ -215,14 +226,14 @@ class AvatarExporter : MonoBehaviour { HipsNotOnGround, HipsSpineChestNotCoincident, TotalBoneCountUnderLimit, - BoneRuleEnd, + AvatarRuleEnd, }; // rules that are treated as errors and prevent exporting, otherwise rules will show as warnings - static readonly BoneRule[] EXPORT_BLOCKING_BONE_RULES = new BoneRule[] { - BoneRule.HipsMapped, - BoneRule.SpineMapped, - BoneRule.ChestMapped, - BoneRule.HeadMapped, + static readonly AvatarRule[] EXPORT_BLOCKING_AVATAR_RULES = new AvatarRule[] { + AvatarRule.HipsMapped, + AvatarRule.SpineMapped, + AvatarRule.ChestMapped, + AvatarRule.HeadMapped, }; class UserBoneInformation { @@ -255,15 +266,63 @@ class AvatarExporter : MonoBehaviour { } } - static Dictionary userBoneInfos = new Dictionary(); - static Dictionary humanoidToUserBoneMappings = new Dictionary(); - static BoneTreeNode userBoneTree = new BoneTreeNode(); - static Dictionary failedBoneRules = new Dictionary(); + class MaterialData { + public Color albedo; + public string albedoMap; + public double metallic; + public string metallicMap; + public double roughness; + public string roughnessMap; + public string normalMap; + public string occlusionMap; + public Color emissive; + public string emissiveMap; + + public string getJSON() { + string json = "{ \"materialVersion\": 1, \"materials\": { "; + json += "\"albedo\": [" + albedo.r + ", " + albedo.g + ", " + albedo.b + "], "; + if (!string.IsNullOrEmpty(albedoMap)) { + json += "\"albedoMap\": \"" + albedoMap + "\", "; + } + json += "\"metallic\": " + metallic + ", "; + if (!string.IsNullOrEmpty(metallicMap)) { + json += "\"metallicMap\": \"" + metallicMap + "\", "; + } + json += "\"roughness\": " + roughness + ", "; + if (!string.IsNullOrEmpty(roughnessMap)) { + json += "\"roughnessMap\": \"" + roughnessMap + "\", "; + } + if (!string.IsNullOrEmpty(normalMap)) { + json += "\"normalMap\": \"" + normalMap + "\", "; + } + if (!string.IsNullOrEmpty(occlusionMap)) { + json += "\"occlusionMap\": \"" + occlusionMap + "\", "; + } + json += "\"emissive\": [" + emissive.r + ", " + emissive.g + ", " + emissive.b + "] "; + if (!string.IsNullOrEmpty(emissiveMap)) { + json += "\", emissiveMap\": \"" + emissiveMap + "\""; + } + json += "} }"; + return json; + } + } static string assetPath = ""; static string assetName = ""; + + static ModelImporter modelImporter; static HumanDescription humanDescription; - static Dictionary dependencyTextures = new Dictionary(); + + static Dictionary userBoneInfos = new Dictionary(); + static Dictionary humanoidToUserBoneMappings = new Dictionary(); + static BoneTreeNode userBoneTree = new BoneTreeNode(); + static Dictionary failedAvatarRules = new Dictionary(); + + static Dictionary textureDependencies = new Dictionary(); + static Dictionary materialMappings = new Dictionary(); + static Dictionary materialDatas = new Dictionary(); + static List materialAlternateStandardShader = new List(); + static Dictionary materialUnsupportedShader = new Dictionary(); [MenuItem("High Fidelity/Export New Avatar")] static void ExportNewAvatar() { @@ -280,7 +339,10 @@ class AvatarExporter : MonoBehaviour { EditorUtility.DisplayDialog("About", "High Fidelity, Inc.\nAvatar Exporter\nVersion " + AVATAR_EXPORTER_VERSION, "Ok"); } - static void ExportSelectedAvatar(bool updateAvatar) { + static void ExportSelectedAvatar(bool updateAvatar) { + // ensure everything is saved to file before exporting + AssetDatabase.SaveAssets(); + string[] guids = Selection.assetGUIDs; if (guids.Length != 1) { if (guids.Length == 0) { @@ -292,7 +354,7 @@ class AvatarExporter : MonoBehaviour { } assetPath = AssetDatabase.GUIDToAssetPath(Selection.assetGUIDs[0]); assetName = Path.GetFileNameWithoutExtension(assetPath); - ModelImporter modelImporter = ModelImporter.GetAtPath(assetPath) as ModelImporter; + modelImporter = ModelImporter.GetAtPath(assetPath) as ModelImporter; if (Path.GetExtension(assetPath).ToLower() != ".fbx" || modelImporter == null) { EditorUtility.DisplayDialog("Error", "Please select an .fbx model asset to export.", "Ok"); return; @@ -302,26 +364,34 @@ class AvatarExporter : MonoBehaviour { " the Rig section of it's Inspector window.", "Ok"); return; } - + humanDescription = modelImporter.humanDescription; - SetUserBoneInformation(); string textureWarnings = SetTextureDependencies(); + SetBoneAndMaterialInformation(); // check if we should be substituting a bone for a missing UpperChest mapping AdjustUpperChestMapping(); - // format resulting bone rule failure strings - // consider export-blocking bone rules to be errors and show them in an error dialog, - // and also include any other bone rule failures plus texture warnings as warnings in the dialog + // format resulting avatar rule failure strings + // consider export-blocking avatar rules to be errors and show them in an error dialog, + // and also include any other avatar rule failures plus texture warnings as warnings in the dialog string boneErrors = ""; string warnings = ""; - foreach (var failedBoneRule in failedBoneRules) { - if (Array.IndexOf(EXPORT_BLOCKING_BONE_RULES, failedBoneRule.Key) >= 0) { - boneErrors += failedBoneRule.Value + "\n\n"; + foreach (var failedAvatarRule in failedAvatarRules) { + if (Array.IndexOf(EXPORT_BLOCKING_AVATAR_RULES, failedAvatarRule.Key) >= 0) { + boneErrors += failedAvatarRule.Value + "\n\n"; } else { - warnings += failedBoneRule.Value + "\n\n"; + warnings += failedAvatarRule.Value + "\n\n"; } } + foreach (string materialName in materialAlternateStandardShader) { + warnings += "The material " + materialName + " is not using the recommended variation of the Standard shader. " + + "We recommend you change it to Standard (Roughness setup) shader for improved performance.\n\n"; + } + foreach (var material in materialUnsupportedShader) { + warnings += "The material " + material.Key + " is using an unsupported shader " + material.Value + + ". Please change it to a Standard shader type.\n\n"; + } warnings += textureWarnings; if (!string.IsNullOrEmpty(boneErrors)) { // if there are both errors and warnings then warnings will be displayed with errors in the error dialog @@ -334,7 +404,7 @@ class AvatarExporter : MonoBehaviour { EditorUtility.DisplayDialog("Error", boneErrors, "Ok"); return; } - + string documentsFolder = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments); string hifiFolder = documentsFolder + "\\High Fidelity Projects"; if (updateAvatar) { // Update Existing Avatar menu option @@ -408,9 +478,9 @@ class AvatarExporter : MonoBehaviour { modelImporter.SaveAndReimport(); // redo parent names, joint mappings, and user bone positions due to the fbx change - // as well as re-check the bone rules for failures + // as well as re-check the avatar rules for failures humanDescription = modelImporter.humanDescription; - SetUserBoneInformation(); + SetBoneAndMaterialInformation(); } } } else { @@ -456,7 +526,7 @@ class AvatarExporter : MonoBehaviour { return; } - // display success dialog with any bone rule warnings + // display success dialog with any avatar rule warnings string successDialog = "Avatar successfully updated!"; if (!string.IsNullOrEmpty(warnings)) { successDialog += "\n\nWarnings:\n" + warnings; @@ -575,6 +645,27 @@ class AvatarExporter : MonoBehaviour { jointOffset.y + ", " + jointOffset.z + ", " + jointOffset.w + ")\n"); } } + + // if there is any material data to save then write out all materials in JSON material format to the materialMap field + if (materialDatas.Count > 0) { + string materialJson = "{ "; + foreach (var materialData in materialDatas) { + // if this is the only material in the mapping and it is the default name No Name mapped to No Name, + // then the avatar has no embedded materials and this material should be applied to all meshes + string materialName = materialData.Key; + if (materialMappings.Count == 1 && materialName == DEFAULT_MATERIAL_NAME && + materialMappings[materialName] == DEFAULT_MATERIAL_NAME) { + materialJson += "\"all\": "; + } else { + materialJson += "\"mat::" + materialName + "\": "; + } + materialJson += materialData.Value.getJSON(); + materialJson += ", "; + } + materialJson = materialJson.Substring(0, materialJson.LastIndexOf(", ")); + materialJson += " }"; + File.AppendAllText(exportFstPath, "materialMap = " + materialJson); + } // open File Explorer to the project directory once finished System.Diagnostics.Process.Start("explorer.exe", "/select," + exportFstPath); @@ -582,11 +673,17 @@ class AvatarExporter : MonoBehaviour { return true; } - static void SetUserBoneInformation() { + static void SetBoneAndMaterialInformation() { userBoneInfos.Clear(); humanoidToUserBoneMappings.Clear(); userBoneTree = new BoneTreeNode(); + materialDatas.Clear(); + materialAlternateStandardShader.Clear(); + materialUnsupportedShader.Clear(); + + SetMaterialMappings(); + // instantiate a game object of the user avatar to traverse the bone tree to gather // bone parents and positions as well as build a bone tree, then destroy it UnityEngine.Object avatarResource = AssetDatabase.LoadAssetAtPath(assetPath, typeof(UnityEngine.Object)); @@ -610,32 +707,40 @@ class AvatarExporter : MonoBehaviour { } } - // generate the list of bone rule failure strings for any bone rules that are not satisfied by this avatar - SetFailedBoneRules(); + // generate the list of avatar rule failure strings for any avatar rules that are not satisfied by this avatar + SetFailedAvatarRules(); } static void TraverseUserBoneTree(Transform modelBone) { GameObject gameObject = modelBone.gameObject; // check if this transform is a node containing mesh, light, or camera instead of a bone - bool mesh = gameObject.GetComponent() != null || gameObject.GetComponent() != null; + MeshRenderer meshRenderer = gameObject.GetComponent(); + SkinnedMeshRenderer skinnedMeshRenderer = gameObject.GetComponent(); + bool mesh = meshRenderer != null || skinnedMeshRenderer != null; bool light = gameObject.GetComponent() != null; bool camera = gameObject.GetComponent() != null; - // if it is in fact a bone, add it to the bone tree as well as user bone infos list with position and parent name - if (!mesh && !light && !camera) { + // if this is a mesh and the model is using external materials then store its material data to be exported + if (mesh && modelImporter.materialLocation == ModelImporterMaterialLocation.External) { + Material[] materials = skinnedMeshRenderer != null ? skinnedMeshRenderer.sharedMaterials : meshRenderer.sharedMaterials; + StoreMaterialData(materials); + } else if (!light && !camera) { + // if it is in fact a bone, add it to the bone tree as well as user bone infos list with position and parent name UserBoneInformation userBoneInfo = new UserBoneInformation(); userBoneInfo.position = modelBone.position; // bone's absolute position string boneName = modelBone.name; if (modelBone.parent == null) { // if no parent then this is actual root bone node of the user avatar, so consider it's parent as "root" + boneName = GetRootBoneName(); // ensure we use the root bone name from the skeleton list for consistency userBoneTree = new BoneTreeNode(boneName); // initialize root of tree userBoneInfo.parentName = "root"; userBoneInfo.boneTreeNode = userBoneTree; } else { // otherwise add this bone node as a child to it's parent's children list - string parentName = modelBone.parent.name; + // if its a child of the root bone, use the root bone name from the skeleton list as the parent for consistency + string parentName = modelBone.parent.parent == null ? GetRootBoneName() : modelBone.parent.name; BoneTreeNode boneTreeNode = new BoneTreeNode(boneName); userBoneInfos[parentName].boneTreeNode.children.Add(boneTreeNode); userBoneInfo.parentName = parentName; @@ -658,7 +763,7 @@ class AvatarExporter : MonoBehaviour { } return result; } - + static void AdjustUpperChestMapping() { if (!humanoidToUserBoneMappings.ContainsKey("UpperChest")) { // if parent of Neck is not Chest then map the parent to UpperChest @@ -682,8 +787,16 @@ class AvatarExporter : MonoBehaviour { } } - static void SetFailedBoneRules() { - failedBoneRules.Clear(); + static string GetRootBoneName() { + // the "root" bone is the first element in the human skeleton bone list + if (humanDescription.skeleton.Length > 0) { + return humanDescription.skeleton[0].name; + } + return ""; + } + + static void SetFailedAvatarRules() { + failedAvatarRules.Clear(); string hipsUserBone = ""; string spineUserBone = ""; @@ -692,60 +805,60 @@ class AvatarExporter : MonoBehaviour { Vector3 hipsPosition = new Vector3(); - // iterate over all bone rules in order and add any rules that fail - // to the failed bone rules map with appropriate error or warning text - for (BoneRule boneRule = 0; boneRule < BoneRule.BoneRuleEnd; ++boneRule) { - switch (boneRule) { - case BoneRule.RecommendedUnityVersion: + // iterate over all avatar rules in order and add any rules that fail + // to the failed avatar rules map with appropriate error or warning text + for (AvatarRule avatarRule = 0; avatarRule < AvatarRule.AvatarRuleEnd; ++avatarRule) { + switch (avatarRule) { + case AvatarRule.RecommendedUnityVersion: if (Array.IndexOf(RECOMMENDED_UNITY_VERSIONS, Application.unityVersion) == -1) { - failedBoneRules.Add(boneRule, "The current version of Unity is not one of the recommended Unity " + - "versions. If you are using a version of Unity later than 2018.2.12f1, " + - "it is recommended to apply Enforce T-Pose under the Pose dropdown " + - "in Humanoid configuration."); + failedAvatarRules.Add(avatarRule, "The current version of Unity is not one of the recommended Unity " + + "versions. If you are using a version of Unity later than 2018.2.12f1, " + + "it is recommended to apply Enforce T-Pose under the Pose dropdown " + + "in Humanoid configuration."); } break; - case BoneRule.SingleRoot: - // bone rule fails if the root bone node has more than one child bone + case AvatarRule.SingleRoot: + // avatar rule fails if the root bone node has more than one child bone if (userBoneTree.children.Count > 1) { - failedBoneRules.Add(boneRule, "There is more than one bone at the top level of the selected avatar's " + - "bone hierarchy. Please ensure all bones for Humanoid mappings are " + - "under the same bone hierarchy."); + failedAvatarRules.Add(avatarRule, "There is more than one bone at the top level of the selected avatar's " + + "bone hierarchy. Please ensure all bones for Humanoid mappings are " + + "under the same bone hierarchy."); } break; - case BoneRule.NoDuplicateMapping: - // bone rule fails if any user bone is mapped to more than one Humanoid bone + case AvatarRule.NoDuplicateMapping: + // avatar rule fails if any user bone is mapped to more than one Humanoid bone foreach (var userBoneInfo in userBoneInfos) { string boneName = userBoneInfo.Key; int mappingCount = userBoneInfo.Value.mappingCount; if (mappingCount > 1) { string text = "The " + boneName + " bone is mapped to more than one bone in Humanoid."; - if (failedBoneRules.ContainsKey(boneRule)) { - failedBoneRules[boneRule] += "\n" + text; + if (failedAvatarRules.ContainsKey(avatarRule)) { + failedAvatarRules[avatarRule] += "\n" + text; } else { - failedBoneRules.Add(boneRule, text); + failedAvatarRules.Add(avatarRule, text); } } } break; - case BoneRule.NoAsymmetricalLegMapping: - CheckAsymmetricalMappingRule(boneRule, LEG_MAPPING_SUFFIXES, "leg"); + case AvatarRule.NoAsymmetricalLegMapping: + CheckAsymmetricalMappingRule(avatarRule, LEG_MAPPING_SUFFIXES, "leg"); break; - case BoneRule.NoAsymmetricalArmMapping: - CheckAsymmetricalMappingRule(boneRule, ARM_MAPPING_SUFFIXES, "arm"); + case AvatarRule.NoAsymmetricalArmMapping: + CheckAsymmetricalMappingRule(avatarRule, ARM_MAPPING_SUFFIXES, "arm"); break; - case BoneRule.NoAsymmetricalHandMapping: - CheckAsymmetricalMappingRule(boneRule, HAND_MAPPING_SUFFIXES, "hand"); + case AvatarRule.NoAsymmetricalHandMapping: + CheckAsymmetricalMappingRule(avatarRule, HAND_MAPPING_SUFFIXES, "hand"); break; - case BoneRule.HipsMapped: - hipsUserBone = CheckHumanBoneMappingRule(boneRule, "Hips"); + case AvatarRule.HipsMapped: + hipsUserBone = CheckHumanBoneMappingRule(avatarRule, "Hips"); break; - case BoneRule.SpineMapped: - spineUserBone = CheckHumanBoneMappingRule(boneRule, "Spine"); + case AvatarRule.SpineMapped: + spineUserBone = CheckHumanBoneMappingRule(avatarRule, "Spine"); break; - case BoneRule.SpineDescendantOfHips: - CheckUserBoneDescendantOfHumanRule(boneRule, spineUserBone, "Hips"); + case AvatarRule.SpineDescendantOfHips: + CheckUserBoneDescendantOfHumanRule(avatarRule, spineUserBone, "Hips"); break; - case BoneRule.ChestMapped: + case AvatarRule.ChestMapped: if (!humanoidToUserBoneMappings.TryGetValue("Chest", out chestUserBone)) { // check to see if there is a child of Spine that we can suggest to be mapped to Chest string spineChild = ""; @@ -755,54 +868,54 @@ class AvatarExporter : MonoBehaviour { spineChild = spineTreeNode.children[0].boneName; } } - failedBoneRules.Add(boneRule, "There is no Chest bone mapped in Humanoid for the selected avatar."); + failedAvatarRules.Add(avatarRule, "There is no Chest bone mapped in Humanoid for the selected avatar."); // if the only found child of Spine is not yet mapped then add it as a suggestion for Chest mapping if (!string.IsNullOrEmpty(spineChild) && !userBoneInfos[spineChild].HasHumanMapping()) { - failedBoneRules[boneRule] += " It is suggested that you map bone " + spineChild + - " to Chest in Humanoid."; + failedAvatarRules[avatarRule] += " It is suggested that you map bone " + spineChild + + " to Chest in Humanoid."; } } break; - case BoneRule.ChestDescendantOfSpine: - CheckUserBoneDescendantOfHumanRule(boneRule, chestUserBone, "Spine"); + case AvatarRule.ChestDescendantOfSpine: + CheckUserBoneDescendantOfHumanRule(avatarRule, chestUserBone, "Spine"); break; - case BoneRule.NeckMapped: - CheckHumanBoneMappingRule(boneRule, "Neck"); + case AvatarRule.NeckMapped: + CheckHumanBoneMappingRule(avatarRule, "Neck"); break; - case BoneRule.HeadMapped: - headUserBone = CheckHumanBoneMappingRule(boneRule, "Head"); + case AvatarRule.HeadMapped: + headUserBone = CheckHumanBoneMappingRule(avatarRule, "Head"); break; - case BoneRule.HeadDescendantOfChest: - CheckUserBoneDescendantOfHumanRule(boneRule, headUserBone, "Chest"); + case AvatarRule.HeadDescendantOfChest: + CheckUserBoneDescendantOfHumanRule(avatarRule, headUserBone, "Chest"); break; - case BoneRule.EyesMapped: + case AvatarRule.EyesMapped: bool leftEyeMapped = humanoidToUserBoneMappings.ContainsKey("LeftEye"); bool rightEyeMapped = humanoidToUserBoneMappings.ContainsKey("RightEye"); if (!leftEyeMapped || !rightEyeMapped) { if (leftEyeMapped && !rightEyeMapped) { - failedBoneRules.Add(boneRule, "There is no RightEye bone mapped in Humanoid " + - "for the selected avatar."); + failedAvatarRules.Add(avatarRule, "There is no RightEye bone mapped in Humanoid " + + "for the selected avatar."); } else if (!leftEyeMapped && rightEyeMapped) { - failedBoneRules.Add(boneRule, "There is no LeftEye bone mapped in Humanoid " + - "for the selected avatar."); + failedAvatarRules.Add(avatarRule, "There is no LeftEye bone mapped in Humanoid " + + "for the selected avatar."); } else { - failedBoneRules.Add(boneRule, "There is no LeftEye or RightEye bone mapped in Humanoid " + - "for the selected avatar."); + failedAvatarRules.Add(avatarRule, "There is no LeftEye or RightEye bone mapped in Humanoid " + + "for the selected avatar."); } } break; - case BoneRule.HipsNotOnGround: + case AvatarRule.HipsNotOnGround: // ensure the absolute Y position for the bone mapped to Hips (if its mapped) is at least HIPS_GROUND_MIN_Y if (!string.IsNullOrEmpty(hipsUserBone)) { UserBoneInformation hipsBoneInfo = userBoneInfos[hipsUserBone]; hipsPosition = hipsBoneInfo.position; if (hipsPosition.y < HIPS_GROUND_MIN_Y) { - failedBoneRules.Add(boneRule, "The bone mapped to Hips in Humanoid (" + hipsUserBone + - ") should not be at ground level."); + failedAvatarRules.Add(avatarRule, "The bone mapped to Hips in Humanoid (" + hipsUserBone + + ") should not be at ground level."); } } break; - case BoneRule.HipsSpineChestNotCoincident: + case AvatarRule.HipsSpineChestNotCoincident: // ensure the bones mapped to Hips, Spine, and Chest are all not in the same position, // check Hips to Spine and Spine to Chest lengths are within HIPS_SPINE_CHEST_MIN_SEPARATION if (!string.IsNullOrEmpty(spineUserBone) && !string.IsNullOrEmpty(chestUserBone) && @@ -813,34 +926,34 @@ class AvatarExporter : MonoBehaviour { Vector3 spineToChest = spineBoneInfo.position - chestBoneInfo.position; if (hipsToSpine.magnitude < HIPS_SPINE_CHEST_MIN_SEPARATION && spineToChest.magnitude < HIPS_SPINE_CHEST_MIN_SEPARATION) { - failedBoneRules.Add(boneRule, "The bone mapped to Hips in Humanoid (" + hipsUserBone + - "), the bone mapped to Spine in Humanoid (" + spineUserBone + - "), and the bone mapped to Chest in Humanoid (" + chestUserBone + - ") should not be coincidental."); + failedAvatarRules.Add(avatarRule, "The bone mapped to Hips in Humanoid (" + hipsUserBone + + "), the bone mapped to Spine in Humanoid (" + spineUserBone + + "), and the bone mapped to Chest in Humanoid (" + chestUserBone + + ") should not be coincidental."); } } break; - case BoneRule.TotalBoneCountUnderLimit: + case AvatarRule.TotalBoneCountUnderLimit: int userBoneCount = userBoneInfos.Count; if (userBoneCount > MAXIMUM_USER_BONE_COUNT) { - failedBoneRules.Add(boneRule, "The total number of bones in the avatar (" + userBoneCount + - ") exceeds the maximum bone limit (" + MAXIMUM_USER_BONE_COUNT + ")."); + failedAvatarRules.Add(avatarRule, "The total number of bones in the avatar (" + userBoneCount + + ") exceeds the maximum bone limit (" + MAXIMUM_USER_BONE_COUNT + ")."); } break; } } } - static string CheckHumanBoneMappingRule(BoneRule boneRule, string humanBoneName) { + static string CheckHumanBoneMappingRule(AvatarRule avatarRule, string humanBoneName) { string userBoneName = ""; - // bone rule fails if bone is not mapped in Humanoid + // avatar rule fails if bone is not mapped in Humanoid if (!humanoidToUserBoneMappings.TryGetValue(humanBoneName, out userBoneName)) { - failedBoneRules.Add(boneRule, "There is no " + humanBoneName + " bone mapped in Humanoid for the selected avatar."); + failedAvatarRules.Add(avatarRule, "There is no " + humanBoneName + " bone mapped in Humanoid for the selected avatar."); } return userBoneName; } - static void CheckUserBoneDescendantOfHumanRule(BoneRule boneRule, string userBoneName, string descendantOfHumanName) { + static void CheckUserBoneDescendantOfHumanRule(AvatarRule avatarRule, string userBoneName, string descendantOfHumanName) { if (string.IsNullOrEmpty(userBoneName)) { return; } @@ -867,13 +980,13 @@ class AvatarExporter : MonoBehaviour { } } - // bone rule fails if no ancestor of given user bone matched the descendant of name (no early return) - failedBoneRules.Add(boneRule, "The bone mapped to " + userBoneInfo.humanName + " in Humanoid (" + userBoneName + - ") is not a child of the bone mapped to " + descendantOfHumanName + " in Humanoid (" + - descendantOfUserBoneName + ")."); + // avatar rule fails if no ancestor of given user bone matched the descendant of name (no early return) + failedAvatarRules.Add(avatarRule, "The bone mapped to " + userBoneInfo.humanName + " in Humanoid (" + userBoneName + + ") is not a child of the bone mapped to " + descendantOfHumanName + " in Humanoid (" + + descendantOfUserBoneName + ")."); } - static void CheckAsymmetricalMappingRule(BoneRule boneRule, string[] mappingSuffixes, string appendage) { + static void CheckAsymmetricalMappingRule(AvatarRule avatarRule, string[] mappingSuffixes, string appendage) { int leftCount = 0; int rightCount = 0; // add Left/Right to each mapping suffix to make Humanoid mapping names, @@ -888,23 +1001,23 @@ class AvatarExporter : MonoBehaviour { ++rightCount; } } - // bone rule fails if number of left appendage mappings doesn't match number of right appendage mappings + // avatar rule fails if number of left appendage mappings doesn't match number of right appendage mappings if (leftCount != rightCount) { - failedBoneRules.Add(boneRule, "The number of bones mapped in Humanoid for the left " + appendage + " (" + - leftCount + ") does not match the number of bones mapped in Humanoid for the right " + - appendage + " (" + rightCount + ")."); + failedAvatarRules.Add(avatarRule, "The number of bones mapped in Humanoid for the left " + appendage + " (" + + leftCount + ") does not match the number of bones mapped in Humanoid for the right " + + appendage + " (" + rightCount + ")."); } } static string GetTextureDirectory(string basePath) { - string textureDirectory = Path.GetDirectoryName(basePath) + "\\textures"; + string textureDirectory = Path.GetDirectoryName(basePath) + "\\" + TEXTURES_DIRECTORY; textureDirectory = textureDirectory.Replace("\\\\", "\\"); return textureDirectory; } - + static string SetTextureDependencies() { string textureWarnings = ""; - dependencyTextures.Clear(); + textureDependencies.Clear(); // build the list of all local asset paths for textures that Unity considers dependencies of the model // for any textures that have duplicate names, return a string of duplicate name warnings @@ -913,11 +1026,11 @@ class AvatarExporter : MonoBehaviour { UnityEngine.Object textureObject = AssetDatabase.LoadAssetAtPath(dependencyPath, typeof(Texture2D)); if (textureObject != null) { string textureName = Path.GetFileName(dependencyPath); - if (dependencyTextures.ContainsKey(textureName)) { + if (textureDependencies.ContainsKey(textureName)) { textureWarnings += "There is more than one texture with the name " + textureName + " referenced in the selected avatar.\n\n"; } else { - dependencyTextures.Add(textureName, dependencyPath); + textureDependencies.Add(textureName, dependencyPath); } } } @@ -927,7 +1040,7 @@ class AvatarExporter : MonoBehaviour { static bool CopyExternalTextures(string texturesDirectory) { // copy the found dependency textures from the local asset folder to the textures folder in the target export project - foreach (var texture in dependencyTextures) { + foreach (var texture in textureDependencies) { string targetPath = texturesDirectory + "\\" + texture.Key; try { File.Copy(texture.Value, targetPath, true); @@ -939,6 +1052,88 @@ class AvatarExporter : MonoBehaviour { } return true; } + + static void StoreMaterialData(Material[] materials) { + // store each material's info in the materialDatas list to be written out later to the FST if it is a supported shader + foreach (Material material in materials) { + string materialName = material.name; + string shaderName = material.shader.name; + + // don't store any material data for unsupported shader types + if (Array.IndexOf(SUPPORTED_SHADERS, shaderName) == -1) { + if (!materialUnsupportedShader.ContainsKey(materialName)) { + materialUnsupportedShader.Add(materialName, shaderName); + } + continue; + } + + MaterialData materialData = new MaterialData(); + materialData.albedo = material.GetColor("_Color"); + materialData.albedoMap = GetMaterialTexture(material, "_MainTex"); + materialData.roughness = material.GetFloat("_Glossiness"); + materialData.roughnessMap = GetMaterialTexture(material, "_SpecGlossMap"); + materialData.normalMap = GetMaterialTexture(material, "_BumpMap"); + materialData.occlusionMap = GetMaterialTexture(material, "_OcclusionMap"); + materialData.emissive = material.GetColor("_EmissionColor"); + materialData.emissiveMap = GetMaterialTexture(material, "_EmissionMap"); + + // for specular setups we will treat the metallic value as the average of the specular RGB intensities + if (shaderName == STANDARD_SPECULAR_SHADER) { + Color specular = material.GetColor("_SpecColor"); + materialData.metallic = (specular.r + specular.g + specular.b) / 3.0f; + } else { + materialData.metallic = material.GetFloat("_Metallic"); + materialData.metallicMap = GetMaterialTexture(material, "_MetallicGlossMap"); + } + + // for non-roughness Standard shaders give a warning that is not the recommended Standard shader, + // and invert smoothness for roughness + if (shaderName == STANDARD_SHADER || shaderName == STANDARD_SPECULAR_SHADER) { + if (!materialAlternateStandardShader.Contains(materialName)) { + materialAlternateStandardShader.Add(materialName); + } + materialData.roughness = 1.0f - materialData.roughness; + } + + // remap the material name from the Unity material name to the fbx material name that it overrides + if (materialMappings.ContainsKey(materialName)) { + materialName = materialMappings[materialName]; + } + if (!materialDatas.ContainsKey(materialName)) { + materialDatas.Add(materialName, materialData); + } + } + } + + static string GetMaterialTexture(Material material, string textureProperty) { + // ensure the texture property name exists in this material and return its texture directory path if so + string[] textureNames = material.GetTexturePropertyNames(); + if (Array.IndexOf(textureNames, textureProperty) >= 0) { + Texture texture = material.GetTexture(textureProperty); + if (texture) { + foreach (var textureDependency in textureDependencies) { + string textureFile = textureDependency.Key; + if (Path.GetFileNameWithoutExtension(textureFile) == texture.name) { + return TEXTURES_DIRECTORY + "/" + textureFile; + } + } + } + } + return ""; + } + + static void SetMaterialMappings() { + materialMappings.Clear(); + + // store the mappings from fbx material name to the Unity material name overriding it using external fbx mapping + var objectMap = modelImporter.GetExternalObjectMap(); + foreach (var mapping in objectMap) { + var material = mapping.Value as UnityEngine.Material; + if (material != null) { + materialMappings.Add(material.name, mapping.Key.name); + } + } + } } class ExportProjectWindow : EditorWindow { diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs.meta b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs.meta deleted file mode 100644 index 373aecc6a8..0000000000 --- a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 00403fdc52187214c8418bc0a7f387e2 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/tools/unity-avatar-exporter/Assets/README.txt b/tools/unity-avatar-exporter/Assets/README.txt index b81a620406..b1e8a5c537 100644 --- a/tools/unity-avatar-exporter/Assets/README.txt +++ b/tools/unity-avatar-exporter/Assets/README.txt @@ -1,6 +1,6 @@ High Fidelity, Inc. Avatar Exporter -Version 0.2 +Version 0.3.1 Note: It is recommended to use Unity versions between 2017.4.17f1 and 2018.2.12f1 for this Avatar Exporter. diff --git a/tools/unity-avatar-exporter/Assets/README.txt.meta b/tools/unity-avatar-exporter/Assets/README.txt.meta deleted file mode 100644 index 148fd21fdd..0000000000 --- a/tools/unity-avatar-exporter/Assets/README.txt.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 30b2b6221fd08234eb07c4d6d525d32e -TextScriptImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/tools/unity-avatar-exporter/avatarExporter.unitypackage b/tools/unity-avatar-exporter/avatarExporter.unitypackage index 95c000e7c6..281cc80ddb 100644 Binary files a/tools/unity-avatar-exporter/avatarExporter.unitypackage and b/tools/unity-avatar-exporter/avatarExporter.unitypackage differ