From 46768c2a6e516d6be213839939f4663cc9bd1f36 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Sat, 16 Feb 2019 00:42:19 +0100 Subject: [PATCH 01/87] remove the code that prevented the crash from happening, since it is not reproducible anymore --- scripts/system/modules/createWindow.js | 3 --- 1 file changed, 3 deletions(-) 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) { From 77d4060fdb8b78ac2e1bdfeffaa9d75502bb3861 Mon Sep 17 00:00:00 2001 From: amer cerkic Date: Thu, 21 Feb 2019 11:37:50 -0800 Subject: [PATCH 02/87] bypassing onStop invoke.Delegate command which prevents the disconnect from Java hooks that are not initiated on restart. On Destroy, the onstop function is called to allow the hooks to finally disconnect and then terminate the app. --- .../oculus/OculusMobileActivity.java | 24 +++++++++++-------- .../qt5/android/bindings/QtActivity.java | 7 +++++- 2 files changed, 20 insertions(+), 11 deletions(-) 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 9ab07bb4dd..2aa7b4da05 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 @@ -34,7 +34,6 @@ public class OculusMobileActivity extends QtActivity implements SurfaceHolder.Ca private native void questNativeOnResume(); private native void questOnAppAfterLoad(); - private SurfaceView mView; private SurfaceHolder mSurfaceHolder; @@ -57,12 +56,15 @@ public class OculusMobileActivity extends QtActivity implements SurfaceHolder.Ca setContentView(mView); questOnAppAfterLoad(); }); + + } @Override protected void onDestroy() { Log.w(TAG, "QQQ onDestroy"); - + isPausing=false; + super.onStop(); nativeOnSurfaceChanged(null); Log.w(TAG, "QQQ onDestroy -- SUPER onDestroy"); @@ -78,6 +80,7 @@ public class OculusMobileActivity extends QtActivity implements SurfaceHolder.Ca questNativeOnResume(); nativeOnResume(); + isPausing=false; } @Override @@ -87,40 +90,41 @@ public class OculusMobileActivity extends QtActivity implements SurfaceHolder.Ca questNativeOnPause(); nativeOnPause(); + isPausing=true; } @Override protected void onStop(){ super.onStop(); - Log.w(TAG, "QQQ Onstop called"); + Log.w(TAG, "QQQ_ Onstop called"); } @Override - protected void onRestart(){ + protected void onRestart() { super.onRestart(); - Log.w(TAG, "QQQ onRestart called ****"); + Log.w(TAG, "QQQ_ onRestart called ****"); questOnAppAfterLoad(); } @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 ***************************************************"); - nativeOnSurfaceChanged(null); - mSurfaceHolder = null; + Log.w(TAG, "QQQ_ surfaceDestroyed ***************************************************"); + // nativeOnSurfaceChanged(null); + // mSurfaceHolder = null; } } \ No newline at end of file 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 46f2af46e7..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,6 +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 isPausing=false; public QtActivity() { } @@ -650,9 +651,13 @@ public class QtActivity extends Activity { @Override protected void onStop() { super.onStop(); - QtApplication.invokeDelegate(); + + if(!isPausing){ + QtApplication.invokeDelegate(); + } } + //--------------------------------------------------------------------------- @Override From fe33aadd698c6cc415b960418ca7de111e877509 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Thu, 21 Feb 2019 14:58:06 -0800 Subject: [PATCH 03/87] Assigning the default world detail and max render rate target in vr quest --- interface/src/LODManager.h | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/interface/src/LODManager.h b/interface/src/LODManager.h index 559bae1779..5817eafc25 100644 --- a/interface/src/LODManager.h +++ b/interface/src/LODManager.h @@ -19,13 +19,20 @@ #include #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.2; // 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; From 9097d9c57cdf1416c32e1a661144d096fd8f8f79 Mon Sep 17 00:00:00 2001 From: amer cerkic Date: Thu, 21 Feb 2019 16:33:25 -0800 Subject: [PATCH 04/87] working on pause -> away mode. Even doesn't seem to be firing. Also found a spot where I left commented out code from lifecycle testing. --- android/apps/questInterface/src/main/cpp/native.cpp | 6 +++++- .../io/highfidelity/oculus/OculusMobileActivity.java | 10 +++++++--- interface/src/AndroidHelper.cpp | 4 ++++ interface/src/AndroidHelper.h | 3 ++- interface/src/Application.cpp | 10 ++++++++++ interface/src/Application.h | 3 ++- scripts/+android_questInterface/defaultScripts.js | 4 ++-- scripts/system/away.js | 3 ++- 8 files changed, 34 insertions(+), 9 deletions(-) diff --git a/android/apps/questInterface/src/main/cpp/native.cpp b/android/apps/questInterface/src/main/cpp/native.cpp index 3c1563c93d..547874b84e 100644 --- a/android/apps/questInterface/src/main/cpp/native.cpp +++ b/android/apps/questInterface/src/main/cpp/native.cpp @@ -61,7 +61,7 @@ extern "C" { Java_io_highfidelity_oculus_OculusMobileActivity_nativeInitOculusPlatform(JNIEnv *env, jobject obj){ initOculusPlatform(env, obj); } -QAndroidJniObject __interfaceActivity; + QAndroidJniObject __interfaceActivity; JNIEXPORT void JNICALL Java_io_highfidelity_oculus_OculusMobileActivity_questNativeOnCreate(JNIEnv *env, jobject obj) { @@ -80,6 +80,10 @@ QAndroidJniObject __interfaceActivity; }); } + 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) { 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 2aa7b4da05..71ccfa84cd 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 @@ -34,6 +34,7 @@ public class OculusMobileActivity extends QtActivity implements SurfaceHolder.Ca private native void questNativeOnResume(); private native void questOnAppAfterLoad(); + private native void questNativeAwayMode(); private SurfaceView mView; private SurfaceHolder mSurfaceHolder; @@ -55,6 +56,7 @@ public class OculusMobileActivity extends QtActivity implements SurfaceHolder.Ca runOnUiThread(() -> { setContentView(mView); questOnAppAfterLoad(); + }); @@ -91,12 +93,14 @@ public class OculusMobileActivity extends QtActivity implements SurfaceHolder.Ca questNativeOnPause(); nativeOnPause(); isPausing=true; + } @Override protected void onStop(){ super.onStop(); Log.w(TAG, "QQQ_ Onstop called"); + questNativeAwayMode(); } @Override @@ -104,6 +108,7 @@ public class OculusMobileActivity extends QtActivity implements SurfaceHolder.Ca super.onRestart(); Log.w(TAG, "QQQ_ onRestart called ****"); questOnAppAfterLoad(); + questNativeAwayMode(); } @Override @@ -123,8 +128,7 @@ public class OculusMobileActivity extends QtActivity implements SurfaceHolder.Ca @Override public void surfaceDestroyed(SurfaceHolder holder) { Log.w(TAG, "QQQ_ surfaceDestroyed ***************************************************"); - // nativeOnSurfaceChanged(null); - // mSurfaceHolder = null; - + nativeOnSurfaceChanged(null); + mSurfaceHolder = null; } } \ No newline at end of file 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 a611738445..a8d0cf6125 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2411,6 +2411,8 @@ 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(); @@ -9135,6 +9137,8 @@ void Application::beforeEnterBackground() { clearDomainOctreeDetails(); } + + void Application::enterBackground() { QMetaObject::invokeMethod(DependencyManager::get().data(), "stop", Qt::BlockingQueuedConnection); @@ -9160,4 +9164,10 @@ void Application::enterForeground() { } #endif +void Application::toggleAwayMode(){ + auto key = QKeyEvent(QEvent::KeyPress,Qt::Key_Escape,Qt::NoModifier); + _keyboardMouseDevice->keyPressEvent(&key); + qDebug()<<"QQQ_ AWAY MODE "; +} + #include "Application.moc" diff --git a/interface/src/Application.h b/interface/src/Application.h index afd9f5f12f..d856297e41 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); diff --git a/scripts/+android_questInterface/defaultScripts.js b/scripts/+android_questInterface/defaultScripts.js index d22716302c..e996f71908 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 diff --git a/scripts/system/away.js b/scripts/system/away.js index 45b6f43b73..c75d58a240 100644 --- a/scripts/system/away.js +++ b/scripts/system/away.js @@ -154,7 +154,7 @@ function goAway(fromStartup) { if (!isEnabled || isAway) { return; } - + console.warn('QQQ_ JS going away); // 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. @@ -176,6 +176,7 @@ function goActive() { return; } + console.warn('QQQ_ JS going active); UserActivityLogger.toggledAway(false); MyAvatar.isAway = false; From 94fc60f28517a2409be134ebfe359fedb09f507f Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Thu, 21 Feb 2019 16:34:50 -0800 Subject: [PATCH 05/87] Bad idea to include global here --- interface/src/LODManager.h | 1 - 1 file changed, 1 deletion(-) diff --git a/interface/src/LODManager.h b/interface/src/LODManager.h index 5817eafc25..87e871a3ab 100644 --- a/interface/src/LODManager.h +++ b/interface/src/LODManager.h @@ -19,7 +19,6 @@ #include #include -#include #ifdef Q_OS_ANDROID const float LOD_DEFAULT_QUALITY_LEVEL = 0.2; // default quality level setting is High (lower framerate) From ddc42585d81e53386c2213b4a8af3aaacc3430a6 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Thu, 21 Feb 2019 16:41:50 -0800 Subject: [PATCH 06/87] Cleanup syntax --- interface/src/LODManager.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/LODManager.h b/interface/src/LODManager.h index 87e871a3ab..77cb1a0d39 100644 --- a/interface/src/LODManager.h +++ b/interface/src/LODManager.h @@ -21,7 +21,7 @@ #ifdef Q_OS_ANDROID -const float LOD_DEFAULT_QUALITY_LEVEL = 0.2; // 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 From ae6a73c71033cd0d4815a7e7f754a8793e835f46 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Thu, 21 Feb 2019 16:42:47 -0800 Subject: [PATCH 07/87] Automatically go to quest-dev domain on startup --- .../src/OculusMobileDisplayPlugin.cpp | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp index 78d80443d8..bc8e1a5113 100644 --- a/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp +++ b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp @@ -168,10 +168,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 +215,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); } From f04fdb2187a8d30fb50ffaf23d2254ee2d729307 Mon Sep 17 00:00:00 2001 From: amer cerkic Date: Fri, 22 Feb 2019 09:11:24 -0800 Subject: [PATCH 08/87] added esc key event to fire for the away animation when cycling pause/resume. Reverted change on surface destroy that nullified the surface. Forgot to remove that during testin. Removed comment in away.js with syntax error. Setting cpu/gpu levels back to 1/1 after vr mode is released. Noticed that OS stays in same cpu/gpu level after app closes. Hoping this will help reduce battery drain for user --- interface/src/Application.cpp | 12 ++++++++---- libraries/oculusMobile/src/ovr/VrHandler.cpp | 4 ++++ scripts/system/away.js | 3 +-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index a8d0cf6125..730f1007a4 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1756,6 +1756,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo #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 @@ -9162,12 +9163,15 @@ void Application::enterForeground() { auto nodeList = DependencyManager::get(); nodeList->setSendDomainServerCheckInEnabled(true); } -#endif + void Application::toggleAwayMode(){ - auto key = QKeyEvent(QEvent::KeyPress,Qt::Key_Escape,Qt::NoModifier); - _keyboardMouseDevice->keyPressEvent(&key); - qDebug()<<"QQQ_ AWAY MODE "; + QKeyEvent event = QKeyEvent (QEvent::KeyPress, Qt::Key_Escape, Qt::NoModifier); + QCoreApplication::sendEvent (this, &event); } + +#endif + + #include "Application.moc" diff --git a/libraries/oculusMobile/src/ovr/VrHandler.cpp b/libraries/oculusMobile/src/ovr/VrHandler.cpp index b3b1416785..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 { diff --git a/scripts/system/away.js b/scripts/system/away.js index c75d58a240..2407678bb7 100644 --- a/scripts/system/away.js +++ b/scripts/system/away.js @@ -154,7 +154,7 @@ function goAway(fromStartup) { if (!isEnabled || isAway) { return; } - console.warn('QQQ_ JS going away); + // 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. @@ -176,7 +176,6 @@ function goActive() { return; } - console.warn('QQQ_ JS going active); UserActivityLogger.toggledAway(false); MyAvatar.isAway = false; From 9c833f7e64f575912216ee9593e13de6e23331a9 Mon Sep 17 00:00:00 2001 From: amer cerkic Date: Fri, 22 Feb 2019 09:21:51 -0800 Subject: [PATCH 09/87] clean up --- .../java/io/highfidelity/oculus/OculusMobileActivity.java | 5 +---- interface/src/Application.cpp | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) 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 71ccfa84cd..7672ddf271 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 @@ -51,15 +51,13 @@ public class OculusMobileActivity extends QtActivity implements SurfaceHolder.Ca nativeOnCreate(); questNativeOnCreate(); } + public void onAppLoadedComplete() { Log.w(TAG, "QQQ Load Completed"); runOnUiThread(() -> { setContentView(mView); questOnAppAfterLoad(); - }); - - } @Override @@ -93,7 +91,6 @@ public class OculusMobileActivity extends QtActivity implements SurfaceHolder.Ca questNativeOnPause(); nativeOnPause(); isPausing=true; - } @Override diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 730f1007a4..e8fed1b2da 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -9166,7 +9166,7 @@ void Application::enterForeground() { void Application::toggleAwayMode(){ - QKeyEvent event = QKeyEvent (QEvent::KeyPress, Qt::Key_Escape, Qt::NoModifier); + QKeyEvent event = QKeyEvent (QEvent::KeyPress, Qt::Key_Escape, Qt::NoModifier); QCoreApplication::sendEvent (this, &event); } From aa8563f3feb96f2a9f7bc48bf7ff9acc0acff786 Mon Sep 17 00:00:00 2001 From: danteruiz Date: Fri, 22 Feb 2019 10:10:34 -0800 Subject: [PATCH 10/87] fix quest ui elements --- .../src/RenderablePolyLineEntityItem.cpp | 15 +++++++- .../src/RenderableShapeEntityItem.cpp | 9 ++++- .../entities-renderer/paintStroke_forward.slp | 1 + .../src/paintStroke_forward.slf | 35 +++++++++++++++++++ .../render-utils/src/RenderForwardTask.cpp | 2 +- 5 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 libraries/entities-renderer/src/entities-renderer/paintStroke_forward.slp create mode 100644 libraries/entities-renderer/src/paintStroke_forward.slf diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp index 64c05b576b..c4ea6a2fea 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); diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index d42a766faa..c3dae762c5 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"); @@ -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/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/render-utils/src/RenderForwardTask.cpp b/libraries/render-utils/src/RenderForwardTask.cpp index df82d4b56d..c6f49bc92a 100755 --- a/libraries/render-utils/src/RenderForwardTask.cpp +++ b/libraries/render-utils/src/RenderForwardTask.cpp @@ -101,7 +101,6 @@ void RenderForwardTask::build(JobModel& task, const render::Varying& input, rend 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(); @@ -114,6 +113,7 @@ void RenderForwardTask::build(JobModel& task, const render::Varying& input, rend // Draw transparent objects forward const auto transparentInputs = DrawForward::Inputs(transparents, lightingModel).asVarying(); task.addJob("DrawTransparents", transparentInputs, shapePlumber); + task.addJob("DrawInFrontTransparent", inFrontTransparentsInputs, false); { // Debug the bounds of the rendered items, still look at the zbuffer From 109eb6288682c4116deee1fdfd3741814144f3f9 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Fri, 22 Feb 2019 11:07:40 -0800 Subject: [PATCH 11/87] requested changes --- libraries/render-utils/src/RenderForwardTask.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/libraries/render-utils/src/RenderForwardTask.cpp b/libraries/render-utils/src/RenderForwardTask.cpp index c6f49bc92a..73692b41c2 100755 --- a/libraries/render-utils/src/RenderForwardTask.cpp +++ b/libraries/render-utils/src/RenderForwardTask.cpp @@ -96,12 +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); - // Draw opaques forward const auto opaqueInputs = DrawForward::Inputs(opaques, lightingModel).asVarying(); task.addJob("DrawOpaques", opaqueInputs, shapePlumber); @@ -113,6 +107,12 @@ void RenderForwardTask::build(JobModel& task, const render::Varying& input, rend // Draw transparent objects forward 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 From 87d98e5b858828bba6a21d73cb496e7aef0b967b Mon Sep 17 00:00:00 2001 From: amantley Date: Tue, 8 Jan 2019 15:26:46 -0800 Subject: [PATCH 12/87] These are the squashed commits for the ik optimization for the Quest Implmented using a new AnimSplineIK node in the anim graph (cherry picked from commit 4fe03ba238659fee7763991f2499a315482b351f) --- CMakeLists.txt | 8 +- .../avatar-animation_withSplineIKNode.json | 2229 +++++++++++++++++ interface/src/avatar/MyAvatar.cpp | 4 + interface/src/avatar/MySkeletonModel.cpp | 30 + libraries/animation/src/AnimContext.h | 1 + .../animation/src/AnimInverseKinematics.cpp | 5 + libraries/animation/src/AnimNodeLoader.cpp | 51 + .../src/AnimPoleVectorConstraint.cpp | 2 +- libraries/animation/src/AnimSplineIK.cpp | 473 ++++ libraries/animation/src/AnimSplineIK.h | 104 + libraries/animation/src/AnimStateMachine.cpp | 1 - libraries/animation/src/IKTarget.h | 3 +- libraries/animation/src/Rig.cpp | 76 +- .../src/avatars-renderer/Avatar.cpp | 36 + .../src/avatars-renderer/Avatar.h | 9 + libraries/fbx/src/FBXSerializer.cpp | 14 + libraries/shared/src/AvatarConstants.h | 1 + libraries/shared/src/CubicHermiteSpline.h | 41 +- tools/unity-avatar-exporter/Assets/README.txt | 1 + 19 files changed, 3061 insertions(+), 28 deletions(-) create mode 100644 interface/resources/avatar/avatar-animation_withSplineIKNode.json create mode 100644 libraries/animation/src/AnimSplineIK.cpp create mode 100644 libraries/animation/src/AnimSplineIK.h diff --git a/CMakeLists.txt b/CMakeLists.txt index c126dce56a..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) @@ -115,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}) @@ -146,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}) @@ -191,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/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/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index afb7a218f6..ff865172ae 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -2951,6 +2951,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); 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/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 d710e9d8ff..37859c939a 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -866,6 +866,11 @@ 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/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/IKTarget.h b/libraries/animation/src/IKTarget.h index 325a1b40b6..57eaff9c30 100644 --- a/libraries/animation/src/IKTarget.h +++ b/libraries/animation/src/IKTarget.h @@ -35,6 +35,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; } @@ -43,7 +45,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 a9c57a4a15..be6240017f 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"); + Rig::Rig() { // Ensure thread-safe access to the rigRegistry. @@ -1051,16 +1064,29 @@ 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); + _animVars.set("splineIKEnabled", true); + _animVars.set("leftHandIKEnabled", true); + _animVars.set("rightHandIKEnabled", true); + _animVars.set("leftFootIKEnabled", true); + _animVars.set("rightFootIKEnabled", true); + _animVars.set("leftFootPoleVectorEnabled", true); + _animVars.set("rightFootPoleVectorEnabled", true); + } 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; @@ -1251,6 +1277,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) { @@ -1265,6 +1292,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); @@ -1396,8 +1424,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(); @@ -1430,8 +1472,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"); @@ -1445,6 +1490,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(); @@ -1478,8 +1527,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"); @@ -1697,6 +1750,7 @@ bool Rig::calculateElbowPoleVector(int handIndex, int elbowIndex, int armIndex, correctionVector = forwardAmount * frontVector; } poleVector = glm::normalize(attenuationVector + fullPoleVector + correctionVector); + return true; } @@ -1819,7 +1873,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 { diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 395fc3b7b4..b842597b88 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" @@ -1535,11 +1536,13 @@ void Avatar::setModelURLFinished(bool success) { void Avatar::rigReady() { buildUnscaledEyeHeightCache(); computeMultiSphereShapes(); + buildSpine2SplineRatioCache(); } // rig has been reset. void Avatar::rigReset() { clearUnscaledEyeHeightCache(); + clearSpine2SplineRatioCache(); } void Avatar::computeMultiSphereShapes() { @@ -1994,10 +1997,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. diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 1e6893a410..1acee7439f 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -236,6 +236,7 @@ public: virtual bool setAbsoluteJointRotationInObjectFrame(int index, const glm::quat& rotation) override { return false; } virtual bool setAbsoluteJointTranslationInObjectFrame(int index, const glm::vec3& translation) override { return false; } +<<<<<<< HEAD // world-space to avatar-space rigconversion functions /**jsdoc * @function MyAvatar.worldToJointPoint @@ -285,6 +286,10 @@ public: * @returns {Quat} */ Q_INVOKABLE glm::quat jointToWorldRotation(const glm::quat& rotation, const int jointIndex = -1) const; +======= + virtual glm::vec3 getSpine2SplineOffset() const { return _spine2SplineOffset; } + virtual float getSpine2SplineRatio() const { return _spine2SplineRatio; } +>>>>>>> cache the spine2 spline default offset and ratio virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override; virtual void setAttachmentData(const QVector& attachmentData) override; @@ -563,7 +568,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! @@ -669,6 +676,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; diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index 9e7f422b40..30bd527546 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -1288,6 +1288,20 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } joint.inverseBindRotation = joint.inverseDefaultRotation; joint.name = fbxModel.name; +<<<<<<< HEAD +======= + if (hfmModel.hfmToHifiJointNameMapping.contains(hfmModel.hfmToHifiJointNameMapping.key(joint.name))) { + joint.name = hfmModel.hfmToHifiJointNameMapping.key(fbxModel.name); + } + + foreach (const QString& childID, _connectionChildMap.values(modelID)) { + QString type = typeFlags.value(childID); + if (!type.isEmpty()) { + hfmModel.hasSkeletonJoints |= (joint.isSkeletonJoint = type.toLower().contains("Skeleton")); + break; + } + } +>>>>>>> implemented the splineIK in animSplineIK.cpp, todo: disable animinversekinematic.cpp joint.bindTransformFoundInCluster = false; 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/tools/unity-avatar-exporter/Assets/README.txt b/tools/unity-avatar-exporter/Assets/README.txt index b81a620406..c84cec2978 100644 --- a/tools/unity-avatar-exporter/Assets/README.txt +++ b/tools/unity-avatar-exporter/Assets/README.txt @@ -2,6 +2,7 @@ High Fidelity, Inc. Avatar Exporter Version 0.2 + Note: It is recommended to use Unity versions between 2017.4.17f1 and 2018.2.12f1 for this Avatar Exporter. To create a new avatar project: From a6ad53e79e74c5cfeb79264431555c0b2ab9198e Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 22 Feb 2019 11:30:26 -0800 Subject: [PATCH 13/87] TEST --- tools/CMakeLists.txt | 2 +- tools/nitpick/CMakeLists.txt | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 6cda67db2d..ed66ab1ed1 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 (NOT STABLE_BUILD) set(ALL_TOOLS udt-test vhacd-util 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 From 9b73c83ebc048a8a2c384332c61d777b5558b247 Mon Sep 17 00:00:00 2001 From: amantley Date: Fri, 22 Feb 2019 11:50:38 -0800 Subject: [PATCH 14/87] removed merge markers (cherry picked from commit 4286142daac70269de155c99c9bbdb1f951eaff6) --- .../avatars-renderer/src/avatars-renderer/Avatar.h | 3 --- libraries/fbx/src/FBXSerializer.cpp | 14 -------------- 2 files changed, 17 deletions(-) diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 1acee7439f..3d25c275b1 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -236,7 +236,6 @@ public: virtual bool setAbsoluteJointRotationInObjectFrame(int index, const glm::quat& rotation) override { return false; } virtual bool setAbsoluteJointTranslationInObjectFrame(int index, const glm::vec3& translation) override { return false; } -<<<<<<< HEAD // world-space to avatar-space rigconversion functions /**jsdoc * @function MyAvatar.worldToJointPoint @@ -286,10 +285,8 @@ public: * @returns {Quat} */ Q_INVOKABLE glm::quat jointToWorldRotation(const glm::quat& rotation, const int jointIndex = -1) const; -======= virtual glm::vec3 getSpine2SplineOffset() const { return _spine2SplineOffset; } virtual float getSpine2SplineRatio() const { return _spine2SplineRatio; } ->>>>>>> cache the spine2 spline default offset and ratio virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override; virtual void setAttachmentData(const QVector& attachmentData) override; diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index 30bd527546..9e7f422b40 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -1288,20 +1288,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } joint.inverseBindRotation = joint.inverseDefaultRotation; joint.name = fbxModel.name; -<<<<<<< HEAD -======= - if (hfmModel.hfmToHifiJointNameMapping.contains(hfmModel.hfmToHifiJointNameMapping.key(joint.name))) { - joint.name = hfmModel.hfmToHifiJointNameMapping.key(fbxModel.name); - } - - foreach (const QString& childID, _connectionChildMap.values(modelID)) { - QString type = typeFlags.value(childID); - if (!type.isEmpty()) { - hfmModel.hasSkeletonJoints |= (joint.isSkeletonJoint = type.toLower().contains("Skeleton")); - break; - } - } ->>>>>>> implemented the splineIK in animSplineIK.cpp, todo: disable animinversekinematic.cpp joint.bindTransformFoundInCluster = false; From a87e49bb23ea6c23f979468f624bfe3ec112cb18 Mon Sep 17 00:00:00 2001 From: Dante Ruiz Date: Fri, 22 Feb 2019 15:01:30 -0800 Subject: [PATCH 15/87] start in quest dev --- interface/src/Application.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index a611738445..3864ba30c0 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2414,6 +2414,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo AndroidHelper::instance().notifyLoadComplete(); #endif pauseUntilLoginDetermined(); + +#if defined(Q_OS_ANDROID) + const QString QUEST_DEV = "hifi://quest-dev"; + DependencyManager::get()->handleLookupString(QUEST_DEV); +#endif } void Application::updateVerboseLogging() { From 7cf79c11081209537e74ee123daa42bee963804c Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 22 Feb 2019 15:06:42 -0800 Subject: [PATCH 16/87] Now WITH installation of nitpick. --- tools/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index ed66ab1ed1..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 (NOT STABLE_BUILD) + if (STABLE_BUILD) set(ALL_TOOLS udt-test vhacd-util From 5bf994626ca14723a9cd6cc3104052da48b3c07b Mon Sep 17 00:00:00 2001 From: amer cerkic Date: Fri, 22 Feb 2019 16:28:36 -0800 Subject: [PATCH 17/87] scaling culling projection matrix by 1.5 to help reduce teh ugly effect of stuff getting cut off too early --- .../oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp index bc8e1a5113..3b8bf50724 100644 --- a/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp +++ b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp @@ -120,7 +120,10 @@ QRectF OculusMobileDisplayPlugin::getPlayAreaRect() { } glm::mat4 OculusMobileDisplayPlugin::getEyeProjection(Eye eye, const glm::mat4& baseProjection) const { + qDebug()<< "QQQ_ " << __FUNCTION__; + glm::mat4 result = baseProjection; + VrHandler::withOvrMobile([&](ovrMobile* session){ auto trackingState = vrapi_GetPredictedTracking2(session, 0.0); result = ovr::Fov{ trackingState.Eye[eye].ProjectionMatrix }.withZ(baseProjection); @@ -136,9 +139,13 @@ glm::mat4 OculusMobileDisplayPlugin::getCullingProjection(const glm::mat4& baseP 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; } From 5c1e5e0c460f3fc2ce2771bac33d52a7a433b22e Mon Sep 17 00:00:00 2001 From: amer cerkic Date: Fri, 22 Feb 2019 16:30:17 -0800 Subject: [PATCH 18/87] cleanup --- .../oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp index 3b8bf50724..cf82d7aba5 100644 --- a/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp +++ b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp @@ -120,8 +120,6 @@ QRectF OculusMobileDisplayPlugin::getPlayAreaRect() { } glm::mat4 OculusMobileDisplayPlugin::getEyeProjection(Eye eye, const glm::mat4& baseProjection) const { - qDebug()<< "QQQ_ " << __FUNCTION__; - glm::mat4 result = baseProjection; VrHandler::withOvrMobile([&](ovrMobile* session){ @@ -142,10 +140,9 @@ glm::mat4 OculusMobileDisplayPlugin::getCullingProjection(const glm::mat4& baseP fovs[0].extend(fovs[1]); result= glm::scale( fovs[0].withZ(baseProjection),glm::vec3(1.5f)); + return result; }); - - return result; } From dab0df1113ffe663f93ff20246353b6bf2408cfe Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Fri, 22 Feb 2019 18:03:23 -0800 Subject: [PATCH 19/87] Trying to fix the damn gamma --- .../display-plugins/OpenGLDisplayPlugin.cpp | 5 ++++- .../src/display-plugins/SrgbToLinear.slf | 3 ++- .../src/OculusMobileDisplayPlugin.cpp | 13 ++++++++++-- .../utilities/render/deferredLighting.qml | 21 +++++++++++++++++++ 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 20fc9a2290..28de13d8c2 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -516,6 +516,7 @@ void OpenGLDisplayPlugin::renderFromTexture(gpu::Batch& batch, const gpu::Textur #ifndef USE_GLES batch.setPipeline(_presentPipeline); #else + //batch.setPipeline(_presentPipeline); batch.setPipeline(_simplePipeline); #endif batch.draw(gpu::TRIANGLE_STRIP, 4); @@ -630,7 +631,8 @@ void OpenGLDisplayPlugin::compositeScene() { batch.setStateScissorRect(ivec4(uvec2(), _compositeFramebuffer->getSize())); batch.resetViewTransform(); batch.setProjectionTransform(mat4()); - batch.setPipeline(_simplePipeline); + // batch.setPipeline(_simplePipeline); + batch.setPipeline(_presentPipeline); batch.setResourceTexture(0, _currentFrame->framebuffer->getRenderBuffer(0)); batch.draw(gpu::TRIANGLE_STRIP, 4); }); @@ -885,6 +887,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 index 8b324c81a5..428ec821a6 100644 --- a/libraries/display-plugins/src/display-plugins/SrgbToLinear.slf +++ b/libraries/display-plugins/src/display-plugins/SrgbToLinear.slf @@ -18,5 +18,6 @@ vec3 colorToLinearRGB(vec3 srgb) { void main(void) { outFragColor.a = 1.0; - outFragColor.rgb = colorToLinearRGB(texture(colorMap, varTexCoord0).rgb); + // outFragColor.rgb = colorToLinearRGB(texture(colorMap, varTexCoord0).rgb); + outFragColor.rgb = pow(texture(colorMap, varTexCoord0).rgb, vec3(1.0 / 2.2)); } diff --git a/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp index bc8e1a5113..ea1a81c4ae 100644 --- a/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp +++ b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp @@ -6,6 +6,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include "OculusMobileDisplayPlugin.h" +#include "../../oculusMobile/src/ovr/Helpers.h" #include #include @@ -58,7 +59,7 @@ void OculusMobileDisplayPlugin::deinit() { bool OculusMobileDisplayPlugin::internalActivate() { _renderTargetSize = { 1024, 512 }; - _cullingProjection = ovr::toGlm(ovrMatrix4f_CreateProjectionFov(90.0f, 90.0f, 0.0f, 0.0f, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP)); + _cullingProjection = ovr::toGlm(ovrMatrix4f_CreateProjectionFov(90.0f, 90.0f, 90.0f, 90.0f, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP)); withOvrJava([&](const ovrJava* java){ @@ -130,6 +131,7 @@ 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]; @@ -137,7 +139,14 @@ glm::mat4 OculusMobileDisplayPlugin::getCullingProjection(const glm::mat4& baseP fovs[i].extract(trackingState.Eye[i].ProjectionMatrix); } fovs[0].extend(fovs[1]); - return fovs[0].withZ(baseProjection); + float horizontalMargin = 1.1f; + float verticalMargin = 1.5f; + fovs[0].leftRightUpDown[0] *= horizontalMargin; + fovs[0].leftRightUpDown[1] *= horizontalMargin; + fovs[0].leftRightUpDown[2] *= verticalMargin; + fovs[0].leftRightUpDown[3] *= verticalMargin; + + return fovs[0].withZ(baseProjection); }); return result; } diff --git a/scripts/developer/utilities/render/deferredLighting.qml b/scripts/developer/utilities/render/deferredLighting.qml index f5c0b8c5da..d147585212 100644 --- a/scripts/developer/utilities/render/deferredLighting.qml +++ b/scripts/developer/utilities/render/deferredLighting.qml @@ -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 From 4e9d162172c7c28c3d813373e1d8d4566ec164a3 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sun, 24 Feb 2019 13:23:20 +1300 Subject: [PATCH 20/87] AccountServices, HifiAbout, and WalletScriptingInterface JSDoc review --- interface/src/AboutUtil.h | 14 ++++---- interface/src/commerce/Wallet.h | 8 ++--- .../AccountServicesScriptingInterface.cpp | 4 +-- .../AccountServicesScriptingInterface.h | 32 +++++++++---------- .../src/scripting/WalletScriptingInterface.h | 22 ++++++------- 5 files changed, 40 insertions(+), 40 deletions(-) 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/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/scripting/AccountServicesScriptingInterface.cpp b/interface/src/scripting/AccountServicesScriptingInterface.cpp index a3597886e9..5ede7c7a98 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 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..b5c4a6e0df 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 @@ -100,7 +100,7 @@ public slots: /**jsdoc * Prompts the user to log in (the login dialog is displayed) 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/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} From 26ad42b9657e9c348f4bd3f637a5da3bef57a79c Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Sun, 24 Feb 2019 19:07:56 -0800 Subject: [PATCH 21/87] FIxing the gamma correction for Quest, intoducing simple gpu lib conversion shaders --- .../display-plugins/OpenGLDisplayPlugin.cpp | 22 +++++++++++----- libraries/gpu/src/gpu/Color.slh | 21 ++++++++++++--- .../src/gpu/DrawTextureGammaLinearToSRGB.slf | 26 +++++++++++++++++++ .../src/gpu/DrawTextureGammaLinearToSRGB.slp | 1 + .../src/gpu/DrawTextureGammaSRGBToLinear.slf | 26 +++++++++++++++++++ .../src/gpu/DrawTextureGammaSRGBToLinear.slp | 1 + .../oculusMobile/src/ovr/Framebuffer.cpp | 11 ++++---- 7 files changed, 93 insertions(+), 15 deletions(-) create mode 100644 libraries/gpu/src/gpu/DrawTextureGammaLinearToSRGB.slf create mode 100644 libraries/gpu/src/gpu/DrawTextureGammaLinearToSRGB.slp create mode 100644 libraries/gpu/src/gpu/DrawTextureGammaSRGBToLinear.slf create mode 100644 libraries/gpu/src/gpu/DrawTextureGammaSRGBToLinear.slp diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 28de13d8c2..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); @@ -516,7 +526,6 @@ void OpenGLDisplayPlugin::renderFromTexture(gpu::Batch& batch, const gpu::Textur #ifndef USE_GLES batch.setPipeline(_presentPipeline); #else - //batch.setPipeline(_presentPipeline); batch.setPipeline(_simplePipeline); #endif batch.draw(gpu::TRIANGLE_STRIP, 4); @@ -631,8 +640,7 @@ void OpenGLDisplayPlugin::compositeScene() { batch.setStateScissorRect(ivec4(uvec2(), _compositeFramebuffer->getSize())); batch.resetViewTransform(); batch.setProjectionTransform(mat4()); - // batch.setPipeline(_simplePipeline); - batch.setPipeline(_presentPipeline); + batch.setPipeline(_simplePipeline); batch.setResourceTexture(0, _currentFrame->framebuffer->getRenderBuffer(0)); batch.draw(gpu::TRIANGLE_STRIP, 4); }); diff --git a/libraries/gpu/src/gpu/Color.slh b/libraries/gpu/src/gpu/Color.slh index 65ddc0b01e..af61e5a34b 100644 --- a/libraries/gpu/src/gpu/Color.slh +++ b/libraries/gpu/src/gpu/Color.slh @@ -16,10 +16,10 @@ // 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) { @@ -30,6 +30,21 @@ 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)); +} + +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..017df1d88d --- /dev/null +++ b/libraries/gpu/src/gpu/DrawTextureGammaLinearToSRGB.slf @@ -0,0 +1,26 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// DrawTexture.frag +// +// Draw texture 0 fetched at texcoord.xy +// +// Created by Sam Gateau on 6/22/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 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..048384fe6c --- /dev/null +++ b/libraries/gpu/src/gpu/DrawTextureGammaSRGBToLinear.slf @@ -0,0 +1,26 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// DrawTexture.frag +// +// Draw texture 0 fetched at texcoord.xy +// +// Created by Sam Gateau on 6/22/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 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/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"); From 4ee2b4322951e46f20355107758d6aea96c58245 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Sun, 24 Feb 2019 20:49:51 -0800 Subject: [PATCH 22/87] FIxing the gamma correction for Quest, intoducing simple gpu lib conversion shaders --- .../src/display-plugins/SrgbToLinear.slf | 23 ------------------- .../src/display-plugins/SrgbToLinear.slp | 1 - libraries/gpu/src/gpu/Color.slh | 8 +++++-- 3 files changed, 6 insertions(+), 26 deletions(-) delete mode 100644 libraries/display-plugins/src/display-plugins/SrgbToLinear.slf delete mode 100644 libraries/display-plugins/src/display-plugins/SrgbToLinear.slp 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 428ec821a6..0000000000 --- a/libraries/display-plugins/src/display-plugins/SrgbToLinear.slf +++ /dev/null @@ -1,23 +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); - outFragColor.rgb = pow(texture(colorMap, varTexCoord0).rgb, vec3(1.0 / 2.2)); -} 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/gpu/src/gpu/Color.slh b/libraries/gpu/src/gpu/Color.slh index af61e5a34b..c676e66c6c 100644 --- a/libraries/gpu/src/gpu/Color.slh +++ b/libraries/gpu/src/gpu/Color.slh @@ -23,7 +23,10 @@ float color_scalar_sRGBToLinear(float value) { } 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) { @@ -38,7 +41,8 @@ float color_scalar_LinearTosRGB(float value) { 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 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) { From df6d8f3be00c621023405f6b9bbfe5d3ab333755 Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Sun, 24 Feb 2019 21:31:28 -0800 Subject: [PATCH 23/87] FIxing the gamma correction for Quest, intoducing simple gpu lib conversion shaders --- libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp index 7943d058bd..a63b954f02 100644 --- a/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp +++ b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp @@ -6,7 +6,6 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include "OculusMobileDisplayPlugin.h" -#include "../../oculusMobile/src/ovr/Helpers.h" #include #include From 2bcee4e454f4a727800e3d6081a9367079b319ae Mon Sep 17 00:00:00 2001 From: nimisha20 Date: Mon, 25 Feb 2019 08:54:47 -0800 Subject: [PATCH 24/87] Update AccountServicesScriptingInterface.cpp Changes according to ctrlaltdavid's comments. --- interface/src/scripting/AccountServicesScriptingInterface.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/scripting/AccountServicesScriptingInterface.cpp b/interface/src/scripting/AccountServicesScriptingInterface.cpp index 5ede7c7a98..5f8fb065ff 100644 --- a/interface/src/scripting/AccountServicesScriptingInterface.cpp +++ b/interface/src/scripting/AccountServicesScriptingInterface.cpp @@ -115,7 +115,7 @@ DownloadInfoResult::DownloadInfoResult() : /**jsdoc * Information on the assets currently being downloaded and pending download. * @typedef {object} AccountServices.DownloadInfoResult - * @property {number[]} downloading - The download percentage of each asset currently downloading. + * @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) { From e20f13f0ae8a17aa01ed0911a86228e94a905dea Mon Sep 17 00:00:00 2001 From: nimisha20 Date: Mon, 25 Feb 2019 09:28:56 -0800 Subject: [PATCH 25/87] Update AccountServicesScriptingInterface.h Adding ctrlaltdavid's changes --- interface/src/scripting/AccountServicesScriptingInterface.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/scripting/AccountServicesScriptingInterface.h b/interface/src/scripting/AccountServicesScriptingInterface.h index b5c4a6e0df..415b405e53 100644 --- a/interface/src/scripting/AccountServicesScriptingInterface.h +++ b/interface/src/scripting/AccountServicesScriptingInterface.h @@ -98,7 +98,7 @@ public slots: bool isLoggedIn(); /**jsdoc - * Prompts the user to log in (the login dialog is displayed) if they're not already logged in. + * Prompts the user to log in (with a login dialog) if they're not already logged in. The function returns if the user is logged in or not before the user completes the login dialog. * @function AccountServices.checkAndSignalForAccessToken * @returns {boolean} true if the user is logged in, false if not. */ From b563f21037878bb28812c9100d0cbedd5871809e Mon Sep 17 00:00:00 2001 From: Sam Gateau Date: Mon, 25 Feb 2019 09:32:27 -0800 Subject: [PATCH 26/87] Fix comments --- libraries/gpu/src/gpu/DrawTextureGammaLinearToSRGB.slf | 8 ++++---- libraries/gpu/src/gpu/DrawTextureGammaSRGBToLinear.slf | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/libraries/gpu/src/gpu/DrawTextureGammaLinearToSRGB.slf b/libraries/gpu/src/gpu/DrawTextureGammaLinearToSRGB.slf index 017df1d88d..3ca3a92f01 100644 --- a/libraries/gpu/src/gpu/DrawTextureGammaLinearToSRGB.slf +++ b/libraries/gpu/src/gpu/DrawTextureGammaLinearToSRGB.slf @@ -2,12 +2,12 @@ <$VERSION_HEADER$> // Generated on <$_SCRIBE_DATE$> // -// DrawTexture.frag +// DrawTextureGammaLinearToSRGB.frag // -// Draw texture 0 fetched at texcoord.xy +// Draw texture 0 fetched at texcoord.xy, and apply linear to sRGB color space conversion // -// Created by Sam Gateau on 6/22/2015 -// Copyright 2015 High Fidelity, Inc. +// 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 diff --git a/libraries/gpu/src/gpu/DrawTextureGammaSRGBToLinear.slf b/libraries/gpu/src/gpu/DrawTextureGammaSRGBToLinear.slf index 048384fe6c..870967ec3a 100644 --- a/libraries/gpu/src/gpu/DrawTextureGammaSRGBToLinear.slf +++ b/libraries/gpu/src/gpu/DrawTextureGammaSRGBToLinear.slf @@ -2,12 +2,12 @@ <$VERSION_HEADER$> // Generated on <$_SCRIBE_DATE$> // -// DrawTexture.frag +// DrawTextureGammaSRGBToLinear.frag // -// Draw texture 0 fetched at texcoord.xy +// Draw texture 0 fetched at texcoord.xy, and apply sRGB to Linear color space conversion // -// Created by Sam Gateau on 6/22/2015 -// Copyright 2015 High Fidelity, Inc. +// 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 From 79d0a0a0a8a82e8a6ac1e482b18936f81786c11a Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Mon, 25 Feb 2019 19:28:38 +0100 Subject: [PATCH 27/87] avatar doctor project status --- .../qml/hifi/avatarPackager/AvatarProject.qml | 82 ++++++++++++++++++- interface/src/avatar/AvatarProject.h | 5 +- 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index 85ef821a4a..a92739cf8a 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: "It seems your project has a few issues that will affect how it works in High Fidelity. " + } + } + ] + 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,29 @@ 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: showErrorsLink + + color: 'white' + linkColor: '#00B4EF' + + visible: AvatarPackagerCore.currentAvatarProject && AvatarPackagerCore.currentAvatarProject.hasErrors + + anchors { + top: infoMessage.bottom + topMargin: 28 + horizontalCenter: parent.horizontalCenter + } + + size: 28 + + text: "View all errors" + + onLinkActivated: { + avatarPackager.state = AvatarPackagerState.avatarDoctorErrorReport; + } + } + HifiControls.Button { id: openFolderButton 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. From 5831db30891244ebdf9658e502080517a7d5030d Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 22 Feb 2019 13:35:52 -0800 Subject: [PATCH 28/87] Build quest demo apk on Jenkins --- android/apps/questInterface/build.gradle | 19 +++++++------ android/apps/questInterface/keystore.jks | Bin 0 -> 2223 bytes android/build_android.sh | 33 ++++++++++++++++++----- android/containerized_build.sh | 2 ++ 4 files changed, 38 insertions(+), 16 deletions(-) create mode 100644 android/apps/questInterface/keystore.jks diff --git a/android/apps/questInterface/build.gradle b/android/apps/questInterface/build.gradle index 3d13877607..bf600b5df1 100644 --- a/android/apps/questInterface/build.gradle +++ b/android/apps/questInterface/build.gradle @@ -44,10 +44,15 @@ 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 : '' + // Hack for Quest demo + storeFile file("keystore.jks") + storePassword "password" + keyAlias "key0" + keyPassword "password" + // 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 : '' v2SigningEnabled false } } @@ -133,12 +138,6 @@ android { assetList.each { file -> out.println(file) } } } - - variant.outputs.all { - if (RELEASE_NUMBER != '0') { - outputFileName = "app_" + RELEASE_NUMBER + "_" + RELEASE_TYPE + ".apk" - } - } } } diff --git a/android/apps/questInterface/keystore.jks b/android/apps/questInterface/keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..4b646122f6f20c60a69d27f15590bc69a757ef60 GIT binary patch literal 2223 zcmcgs={wX51O3fvX1rrx=GM4~5QE=bW0`ntS)(isLbC5-EMaVA(ny7CEs;T}s4SI2 zg|SDLNJ7XyGE|oA?o-eEKF|FN-Vf))Ip@nc=fm0O>~jDBfC2yj`3<3P=bXA`j(81FAhsS$ zAO5f&X%HvyB`iGj4 zwp#gCx-pp1C~MYb8jor+@}K(fqOz+(FEKiPyzx-2$z8qH zqHbD3rM+OPGOmC1O0m%KOxBp-dbsEAp5ORo$}*c$cKZgXxv-~aT!o*u5%*Vs-HdwU zW^#h@vSv*Y=de+XCtsBLVdL3YdheCo#+lu3yXwJnW8rPr@YKqv#!Df+u2*6?<%co~ z{SrGP@KZs&Yp2f>e0;N{dVM+@<@86K`v$P&JGgt3Zc)$7)lV90uYoNae;B5vLc_9$ zp5G~ZpfF{B9&jt_A^5K`vn`NZ)WCDLr$u}tYDMF6er$P^8dEVx?Wrw#7TQzN!3m`p zbf87ffQf>Dchzji;kkaR4K(%@jV6*!x{8RP^+-Aj3ncL8Pd}gP15)(Se)6$vPJSAC9mo!F%IGAkG>ld9d zIbuym8VlrW80aFhv8U!G^IZyBJ!!3BFW7x3G~SPRU$l70bkJXY*Dq6ZvO+jE8(;2? z*2OWXac@dgchXXm%24@D8QVy1N|l{Hdxg99RG3Lhy~3TMYAbxsa9wL{ez%L1?DqC4 z+Ff{mHK3Mf-V;hOmkW&H%N6bET1>yQw(xdq5-->6ec+h4sOG%Ys|Wl_}+^*VD$)aXn5q+T~o>mG9@H_Wmhd&p(1eT;KbWYuk*>1tN$q-W5g zVaF8fEN)NQMDz0${lMCs{QaaPe~IV7rfet+ehKc&=E03;@@a%cn`TX5=N!F--Om+m zbLcb2V&Bai{eeKLT62tsSH2ECI;a=$NFd!ssfX~g4LUp?fKS$kyL!_y0@`nB_DyFJ ztQRO+@3#b+_K(ALKea<1Q!)_wx{#PH8_q))C!r$DXj1QSmy9>x9Y^11QuFq51|1(8 zseax~9#zJZIW?Tr>6<&Qou4(Wj+aQ_7nby0El(<;`+B}%%KmgNtEDkyecPTnZk%WzDjZ4xGSkm#hUp+)Z0t7 zl(2Yc-H{TY(G|{oy_FlMY54T|sW@pS@9;FU6(Mp%DxPy1qJ;*;JiXfMy3E4kDi)H` zANLnVwY|{UADj{BYNG}F+eM_zSpO7EX3wnJc}K){#T8%86Ffro$TFhL>x5Lb@?Pnv|@#n3R{ zKnVlaaNkhyPf;vUgGl@f1VJL1r0yV!C8-dJMACoh|0e_;a^&CTAqW6QhwuU*9fE<< zArPQUqHp`-n-Mu(TzgUXg*)#2_ghBAwht9Jm9hful2-XWB9h0GzE;tmk)^x#?Wa3` z&We}lbI!Mf)LT~Zl%u|Fl4tIju4KtGj}Du{kPeE}5oltSRs3@0Q=^p--GIjzS|d6= zZ1F+#d|ZCkTD>bepd;0w%xGDsxc5(vE0>^Of04vMyK8lvh~>Mnc`euBmm5AGqUQ-S z@@jhf-Vnl=)^mT^HA5K}QQRcsnpciEJGBh=6swNKoUg zDRfp<_S%C4YRgoO8cq=2G#*8Y`o78#+c9Xvh1V~^R%D6R^H`^IG6&)#&8Riy)9;`# z2mrY!35tVaNSKWn4_pK;l#qF$NwhjkZ?C)Wc_~X>7?eSo|9eF4-y=c*X37vNCh^J) z>1d$^^&-k4*##0_Bik?*LCSTH+ptz!r)s;&1|6teZ)2C*i$CbtX|WVCYQ$fbbGw>7u_XWd7XkpJi?(T?Zp0@ zqJ4=#o%meLIYxha<-wpXv0cs5oTA7lIfdJXkJvf3#q5|RIn^va2$^hR??~2;1a>j+ zPGpc-d&SrWgOE+R+TQ;Dcz_U-z5};j!t84AFtb|uk8nS_z|QU~EV(Fh{;3obuLvzU KB59rbJmw!9zT}$# literal 0 HcmV?d00001 diff --git a/android/build_android.sh b/android/build_android.sh index a066332f9a..106a295750 100755 --- a/android/build_android.sh +++ b/android/build_android.sh @@ -1,11 +1,32 @@ #!/usr/bin/env bash set -xeuo pipefail + +ANDROID_BUILD_TYPE=release +ANDROID_BUILD_TARGET=assembleRelease + +case "$RELEASE_TYPE" in + PR) ANDROID_APK_SUFFIX=PR${RELEASE_NUMBER}-${SHA7}.apk ;; + *) ANDROID_APK_SUFFIX=${RELEASE_NUMBER}-${SHA7}.apk ;; +esac + + +# 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}-unsigned.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} +cp ${ANDROID_OUTPUT_DIR}/${ANDROID_OUTPUT_FILE} ./${ANDROID_APK_NAME} + + + -# 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..34e620ad2e 100755 --- a/android/containerized_build.sh +++ b/android/containerized_build.sh @@ -9,6 +9,7 @@ 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 +# 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 +28,7 @@ docker run \ -e OAUTH_CLIENT_SECRET \ -e OAUTH_CLIENT_ID \ -e OAUTH_REDIRECT_URI \ + -e SHA7 \ -e VERSION_CODE \ "${DOCKER_IMAGE_NAME}" \ sh -c "./build_android.sh" From 07753927590ef3af2d1aa51f22dd09ed1850f5f7 Mon Sep 17 00:00:00 2001 From: amer cerkic Date: Mon, 25 Feb 2019 14:05:54 -0800 Subject: [PATCH 29/87] adding market place back to the default scripts --- scripts/+android_questInterface/defaultScripts.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/+android_questInterface/defaultScripts.js b/scripts/+android_questInterface/defaultScripts.js index e996f71908..c294537419 100644 --- a/scripts/+android_questInterface/defaultScripts.js +++ b/scripts/+android_questInterface/defaultScripts.js @@ -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", From 650b34c463fb46da3912f6db146d5f965fdb8f84 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 25 Feb 2019 18:00:56 -0800 Subject: [PATCH 30/87] adding clock --- interface/resources/fonts/rawline-500.ttf | Bin 0 -> 262160 bytes .../resources/qml/hifi/tablet/TabletHome.qml | 143 ++++++++++++++---- .../resources/qml/styles-uit/Rawline.qml | 20 +++ interface/resources/qml/stylesUit/Rawline.qml | 20 +++ interface/src/Application.cpp | 1 + 5 files changed, 156 insertions(+), 28 deletions(-) create mode 100644 interface/resources/fonts/rawline-500.ttf create mode 100644 interface/resources/qml/styles-uit/Rawline.qml create mode 100644 interface/resources/qml/stylesUit/Rawline.qml diff --git a/interface/resources/fonts/rawline-500.ttf b/interface/resources/fonts/rawline-500.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a0e18c7364f49252d59226d0959b994a4156b3ba GIT binary patch literal 262160 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%0DJ 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

G#Mh;)ywUqNlIcrQ=}Lr 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 GKls0o2tK3$ literal 0 HcmV?d00001 diff --git a/interface/resources/qml/hifi/tablet/TabletHome.qml b/interface/resources/qml/hifi/tablet/TabletHome.qml index 4d7883522e..94a65e70a0 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,131 @@ 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 + visible: !Account.loggedIn + text: qsTr("Log in") + horizontalAlignment: Text.AlignRight + Layout.alignment: Qt.AlignRight + font.pixelSize: 18 + font.family: "Rawline" + color: "#afafaf" + } + TextMetrics { + id: loginTextMetrics + text: Account.loggedIn ? tabletRoot.usernameShort : loginText.text + font: loginText.font + } + + HifiStylesUit.ShortcutText { + anchors.right: parent.right + visible: Account.loggedIn + text: "" + tabletRoot.usernameShort + "" + horizontalAlignment: Text.AlignRight + Layout.alignment: Qt.AlignRight + font.pixelSize: 18 + font.bold: true + font.family: "Rawline" + linkColor: hifi.colors.blueAccent + } + + MouseArea { + anchors.fill: parent + onClicked: { + if (!Account.loggedIn) { + DialogsManager.showLoginDialog(); + } else { + Account.logOut(); + } } } } + Component.onCompleted: { + rightContainer.timeChanged(); + } } } 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/Application.cpp b/interface/src/Application.cpp index 83b287b7ae..a850cdfe12 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1075,6 +1075,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"); From 4825ecdbf35c78b624c739128a790f147530e52c Mon Sep 17 00:00:00 2001 From: nimisha20 Date: Mon, 25 Feb 2019 22:44:16 -0800 Subject: [PATCH 31/87] Update AccountServicesScriptingInterface.h --- interface/src/scripting/AccountServicesScriptingInterface.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/scripting/AccountServicesScriptingInterface.h b/interface/src/scripting/AccountServicesScriptingInterface.h index 415b405e53..1555caba3c 100644 --- a/interface/src/scripting/AccountServicesScriptingInterface.h +++ b/interface/src/scripting/AccountServicesScriptingInterface.h @@ -98,7 +98,7 @@ public slots: bool isLoggedIn(); /**jsdoc - * Prompts the user to log in (with a login dialog) if they're not already logged in. The function returns if the user is logged in or not before the user completes the login dialog. + * Prompts the user to log in (with a login dialog) if they're not already logged in. The function returns a value before the user completes the login dialog. * @function AccountServices.checkAndSignalForAccessToken * @returns {boolean} true if the user is logged in, false if not. */ From 736a050e6808ff0913e819558937d3b06d8d9e3c Mon Sep 17 00:00:00 2001 From: nimisha20 Date: Mon, 25 Feb 2019 22:55:56 -0800 Subject: [PATCH 32/87] Update AccountServicesScriptingInterface.h --- interface/src/scripting/AccountServicesScriptingInterface.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/scripting/AccountServicesScriptingInterface.h b/interface/src/scripting/AccountServicesScriptingInterface.h index 1555caba3c..b5bee6a7e4 100644 --- a/interface/src/scripting/AccountServicesScriptingInterface.h +++ b/interface/src/scripting/AccountServicesScriptingInterface.h @@ -98,7 +98,7 @@ public slots: bool isLoggedIn(); /**jsdoc - * Prompts the user to log in (with a login dialog) if they're not already logged in. The function returns a value before the user completes the login dialog. + * The function returns the log in 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 logged in, false if not. */ From ba3895efdd942ed40d24ad5134b23d5bc05c4756 Mon Sep 17 00:00:00 2001 From: nimisha20 Date: Mon, 25 Feb 2019 22:56:36 -0800 Subject: [PATCH 33/87] Update AccountServicesScriptingInterface.h Minor copy-edit --- interface/src/scripting/AccountServicesScriptingInterface.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/scripting/AccountServicesScriptingInterface.h b/interface/src/scripting/AccountServicesScriptingInterface.h index b5bee6a7e4..b188b4e63b 100644 --- a/interface/src/scripting/AccountServicesScriptingInterface.h +++ b/interface/src/scripting/AccountServicesScriptingInterface.h @@ -98,7 +98,7 @@ public slots: bool isLoggedIn(); /**jsdoc - * The function returns the log in status of the user and prompts the user to log in (with a login dialog) 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 logged in, false if not. */ From db1c78246fc640971ea55f273a1132ac4966e2ce Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Tue, 26 Feb 2019 11:32:43 -0700 Subject: [PATCH 34/87] Read and apply the FBX upVector parameter --- libraries/fbx/src/FBXSerializer.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index 9e7f422b40..3c8aa8f799 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,20 @@ 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_Z) { + upAxisZRotation = glm::angleAxis(glm::radians(-90.0f), glm::vec3(1.0f, 0.0f, 0.0f)); + applyUpAxisZRotation = true; + } } } } @@ -1271,7 +1281,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr hfmModel.hasSkeletonJoints = (hfmModel.hasSkeletonJoints || joint.isSkeletonJoint); 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 +1673,9 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } + if (applyUpAxisZRotation) { + hfmModelPtr->bindExtents.rotate(upAxisZRotation); + } return hfmModelPtr; } From 028cce53949035ebb7611cbf1327426acb8803ef Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Tue, 26 Feb 2019 12:26:50 -0800 Subject: [PATCH 35/87] change short username to 14 characters, displaying as normal text --- .../resources/qml/hifi/tablet/TabletHome.qml | 19 ++----------------- .../resources/qml/hifi/tablet/TabletRoot.qml | 6 +++--- 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/interface/resources/qml/hifi/tablet/TabletHome.qml b/interface/resources/qml/hifi/tablet/TabletHome.qml index 94a65e70a0..a1da69a44a 100644 --- a/interface/resources/qml/hifi/tablet/TabletHome.qml +++ b/interface/resources/qml/hifi/tablet/TabletHome.qml @@ -134,8 +134,7 @@ Item { Text { id: loginText anchors.right: parent.right - visible: !Account.loggedIn - text: qsTr("Log in") + text: Account.loggedIn ? tabletRoot.usernameShort : qsTr("Log in") horizontalAlignment: Text.AlignRight Layout.alignment: Qt.AlignRight font.pixelSize: 18 @@ -144,29 +143,15 @@ Item { } TextMetrics { id: loginTextMetrics - text: Account.loggedIn ? tabletRoot.usernameShort : loginText.text + text: loginText.text font: loginText.font } - HifiStylesUit.ShortcutText { - anchors.right: parent.right - visible: Account.loggedIn - text: "" + tabletRoot.usernameShort + "" - horizontalAlignment: Text.AlignRight - Layout.alignment: Qt.AlignRight - font.pixelSize: 18 - font.bold: true - font.family: "Rawline" - linkColor: hifi.colors.blueAccent - } - MouseArea { anchors.fill: parent onClicked: { if (!Account.loggedIn) { DialogsManager.showLoginDialog(); - } else { - Account.logOut(); } } } 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 + "..." } } From f47ec099273548a1300be6699571727bc29aaf32 Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Tue, 26 Feb 2019 14:01:30 -0700 Subject: [PATCH 36/87] use UP_AXIS_Y --- libraries/fbx/src/FBXSerializer.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index 3c8aa8f799..0b31eda94b 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -484,7 +484,9 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr constexpr int UP_AXIS_Y = 1; constexpr int UP_AXIS_Z = 2; int upAxis = subobject.properties.at(index).toInt(); - if (upAxis == UP_AXIS_Z) { + 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; } From 7039ee471d217a3812586025c2e71498bddcc5de Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Tue, 26 Feb 2019 13:49:55 -0800 Subject: [PATCH 37/87] Move newly-created Connection to NodeList thread --- libraries/networking/src/udt/Connection.cpp | 1 - libraries/networking/src/udt/Socket.cpp | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) 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/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); From 4c6bea48b2a713fce07934df97ed77b3ebd316a7 Mon Sep 17 00:00:00 2001 From: amer cerkic Date: Tue, 26 Feb 2019 14:48:34 -0800 Subject: [PATCH 38/87] fixed a case where _widths = 0 and widths-1 is not valid. --- .../src/RenderablePolyLineEntityItem.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp index c4ea6a2fea..0ea8db7c15 100644 --- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp @@ -184,8 +184,11 @@ void PolyLineEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPo void PolyLineEntityRenderer::updateGeometry() { int maxNumVertices = std::min(_points.length(), _normals.length()); + qDebug()<<"QQQ_ widths size: "<< _widths.size(); + qDebug()<<"QQQ_ MaxNumbVertices: "<= 0) { + if (_widths.size() > 0) { for (int i = 1; i < maxNumVertices; i++) { float width = PolyLineEntityItem::DEFAULT_LINE_WIDTH; if (i < _widths.length()) { @@ -206,12 +209,15 @@ 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; + float width=PolyLineEntityItem::DEFAULT_LINE_WIDTH; + if(_widths.size()>0 && i < _widths.size()) + width = _widths[i]; + if (i > 0) { // First uCoord is 0.0f if (!_isUVModeStretch) { accumulatedDistance += glm::distance(point, _points[i - 1]); From 5a7c8c312640c4975a5b94dc6662056b8be1ea0a Mon Sep 17 00:00:00 2001 From: amer cerkic Date: Tue, 26 Feb 2019 14:53:44 -0800 Subject: [PATCH 39/87] removed debugg helpers --- .../entities-renderer/src/RenderablePolyLineEntityItem.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp index 0ea8db7c15..ec8444ff35 100644 --- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp @@ -183,10 +183,6 @@ void PolyLineEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPo void PolyLineEntityRenderer::updateGeometry() { int maxNumVertices = std::min(_points.length(), _normals.length()); - - qDebug()<<"QQQ_ widths size: "<< _widths.size(); - qDebug()<<"QQQ_ MaxNumbVertices: "< 0) { for (int i = 1; i < maxNumVertices; i++) { @@ -215,8 +211,9 @@ void PolyLineEntityRenderer::updateGeometry() { glm::vec3 point = _points[i]; float width=PolyLineEntityItem::DEFAULT_LINE_WIDTH; - if(_widths.size()>0 && i < _widths.size()) + if(_widths.size()>0 && i < _widths.size()) { width = _widths[i]; + } if (i > 0) { // First uCoord is 0.0f if (!_isUVModeStretch) { From f5852139f75238a3b4b79eebfb63d22ae8ec6da4 Mon Sep 17 00:00:00 2001 From: amer cerkic Date: Tue, 26 Feb 2019 15:05:58 -0800 Subject: [PATCH 40/87] fixing comment since the original check was correct --- .../entities-renderer/src/RenderablePolyLineEntityItem.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp index ec8444ff35..44b368d827 100644 --- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp @@ -209,11 +209,7 @@ void PolyLineEntityRenderer::updateGeometry() { for (int i = 0; i < maxNumVertices; i++) { // Position glm::vec3 point = _points[i]; - - float width=PolyLineEntityItem::DEFAULT_LINE_WIDTH; - if(_widths.size()>0 && i < _widths.size()) { - width = _widths[i]; - } + float width = i < _widths.size() ? _widths[i] : PolyLineEntityItem::DEFAULT_LINE_WIDTH; if (i > 0) { // First uCoord is 0.0f if (!_isUVModeStretch) { From 19d584f0af1a6ebc2910edfe301319b322bdbad0 Mon Sep 17 00:00:00 2001 From: amer cerkic Date: Tue, 26 Feb 2019 15:28:28 -0800 Subject: [PATCH 41/87] added Sams suggestions to the logic --- .../src/RenderablePolyLineEntityItem.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp index 44b368d827..454e8b136a 100644 --- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.cpp @@ -185,15 +185,17 @@ void PolyLineEntityRenderer::updateGeometry() { int maxNumVertices = std::min(_points.length(), _normals.length()); bool doesStrokeWidthVary = false; 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; } } @@ -209,6 +211,7 @@ void PolyLineEntityRenderer::updateGeometry() { 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 From ff7995ae18a9e6262fbb58167912ce9240f9b586 Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Tue, 26 Feb 2019 18:29:55 -0700 Subject: [PATCH 42/87] Right fix --- libraries/fbx/src/FBXSerializer.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index 0b31eda94b..e022ca8921 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -1281,7 +1281,9 @@ 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.isSkeletonJoint) { + joint.rotation *= upAxisZRotation; + } glm::quat combinedRotation = joint.preRotation * joint.rotation * joint.postRotation; if (joint.parentIndex == -1) { joint.transform = hfmModel.offset * glm::translate(joint.translation) * joint.preTransform * @@ -1676,6 +1678,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } if (applyUpAxisZRotation) { + hfmModelPtr->meshExtents.rotate(upAxisZRotation); hfmModelPtr->bindExtents.rotate(upAxisZRotation); } return hfmModelPtr; From 9559f9ffd39ac80fdb7bb6f76c946ee15d5586be Mon Sep 17 00:00:00 2001 From: r3tk0n Date: Tue, 26 Feb 2019 17:52:18 -0800 Subject: [PATCH 43/87] Add off-hand controller manipulation to fargrab. --- .../controllerModules/farGrabEntity.js | 94 +++++++++++++++++-- 1 file changed, 88 insertions(+), 6 deletions(-) diff --git a/scripts/system/controllers/controllerModules/farGrabEntity.js b/scripts/system/controllers/controllerModules/farGrabEntity.js index 197a809e91..d2d8af9dc4 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,46 @@ Script.include("/~/system/libraries/controllers.js"); 100, makeLaserParams(this.hand, false)); + //enableDispatcherModule("LeftFarGrabEntity", leftFarGrabEntity); + //enableDispatcherModule("RightFarGrabEntity", rightFarGrabEntity); + + this.getOtherModule = function () { + // Used to fetch other module. + return getEnabledModuleByName(this.hand === RIGHT_HAND ? ("RightFarGrabEntity") : ("LeftFarGrabEntity")); + }; + + 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.rightTrigger : _this.leftTrigger); + } + + this.shouldManipulateTarget = function () { + return (_this.getOffhandTrigger() > TRIGGER_ON_VALUE) ? true : false; + }; + + 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 +190,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); @@ -231,8 +280,31 @@ Script.include("/~/system/libraries/controllers.js"); // 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 }); + if (this.shouldManipulateTarget()) { + var pose = Controller.getPoseValue((this.getOffhand() ? Controller.Standard.RightHand : Controller.Standard.LeftHand)); + if (pose.valid) { + if (!this.manipulating) { + if (!this.wasManipulating) { + this.initialEntityRotation = this.getTargetRotation(); // Worldframe. + } + this.initialControllerRotation = Quat.multiply(pose.rotation, MyAvatar.orientation); // Worldframe. + this.manipulating = true; + } + } + + var rot = Quat.multiply(pose.rotation, MyAvatar.orientation); + var rotBetween = this.calculateEntityRotationManipulation(rot); + this.lastJointRotation = Quat.multiply(rotBetween, this.initialEntityRotation); + this.setJointRotation(this.lastJointRotation); + } else { + if (this.manipulating) { + this.initialEntityRotation = this.lastJointRotation; + this.wasManipulating = true; + } + this.manipulating = false; + this.initialControllerRotation = Quat.IDENTITY; + } + this.setJointTranslation(newTargetPosLocal); this.previousRoomControllerPosition = roomControllerPosition; }; @@ -254,9 +326,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 +404,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 +416,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, [], []); From 52d80476a8fda0d32abff288815ff75a5f9bae32 Mon Sep 17 00:00:00 2001 From: r3tk0n Date: Tue, 26 Feb 2019 17:55:16 -0800 Subject: [PATCH 44/87] Fix activation criteria. --- scripts/system/controllers/controllerModules/farGrabEntity.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/system/controllers/controllerModules/farGrabEntity.js b/scripts/system/controllers/controllerModules/farGrabEntity.js index d2d8af9dc4..6e08542bda 100644 --- a/scripts/system/controllers/controllerModules/farGrabEntity.js +++ b/scripts/system/controllers/controllerModules/farGrabEntity.js @@ -88,7 +88,7 @@ Script.include("/~/system/libraries/controllers.js"); this.getOtherModule = function () { // Used to fetch other module. - return getEnabledModuleByName(this.hand === RIGHT_HAND ? ("RightFarGrabEntity") : ("LeftFarGrabEntity")); + return getEnabledModuleByName(this.hand === RIGHT_HAND ? ("LeftFarGrabEntity") : ("RightFarGrabEntity")); }; this.getTargetRotation = function () { @@ -105,7 +105,7 @@ Script.include("/~/system/libraries/controllers.js"); } this.getOffhandTrigger = function () { - return (_this.hand === RIGHT_HAND ? _this.rightTrigger : _this.leftTrigger); + return (_this.hand === RIGHT_HAND ? _this.leftTrigger : _this.rightTrigger); } this.shouldManipulateTarget = function () { From ca740c6254229112e3657dd02f2179883ca06ed1 Mon Sep 17 00:00:00 2001 From: r3tk0n Date: Tue, 26 Feb 2019 18:44:00 -0800 Subject: [PATCH 45/87] Add a 2x multiplier for controller movement to entity rotation when manipulating with fargrab. --- .../controllerModules/farGrabEntity.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/scripts/system/controllers/controllerModules/farGrabEntity.js b/scripts/system/controllers/controllerModules/farGrabEntity.js index 6e08542bda..5e621809b2 100644 --- a/scripts/system/controllers/controllerModules/farGrabEntity.js +++ b/scripts/system/controllers/controllerModules/farGrabEntity.js @@ -276,25 +276,31 @@ 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); + + // 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(); // Worldframe. + this.initialEntityRotation = this.getTargetRotation(); } - this.initialControllerRotation = Quat.multiply(pose.rotation, MyAvatar.orientation); // Worldframe. + // 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); - this.lastJointRotation = Quat.multiply(rotBetween, this.initialEntityRotation); + var doubleRot = Quat.multiply(rotBetween, rotBetween); + this.lastJointRotation = Quat.multiply(doubleRot, this.initialEntityRotation); this.setJointRotation(this.lastJointRotation); } else { if (this.manipulating) { From 92cfa49bfb55fc780a1f2a34a437ef6d1001379f Mon Sep 17 00:00:00 2001 From: amer cerkic Date: Wed, 27 Feb 2019 11:42:22 -0800 Subject: [PATCH 46/87] adding and testing command line parameter passing to application --- .../questInterface/src/main/AndroidManifest.xml | 2 +- .../questInterface/InterfaceActivity.java | 15 +++++++++++++++ .../questInterface/PermissionsChecker.java | 16 +++++++++++++++- .../oculus/OculusMobileActivity.java | 11 +++++++++++ interface/src/Application.cpp | 8 ++++---- 5 files changed, 46 insertions(+), 6 deletions(-) diff --git a/android/apps/questInterface/src/main/AndroidManifest.xml b/android/apps/questInterface/src/main/AndroidManifest.xml index a5de47bdce..0f3616612a 100644 --- a/android/apps/questInterface/src/main/AndroidManifest.xml +++ b/android/apps/questInterface/src/main/AndroidManifest.xml @@ -28,7 +28,7 @@ android:excludeFromRecents="true"> - + ()->handleLookupString(QUEST_DEV); +// / const QString QUEST_DEV = "hifi://quest-dev"; + // DependencyManager::get()->handleLookupString(QUEST_DEV); #endif } @@ -3669,8 +3669,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); From 29ec5486f6f49d288692eb2c4e140d4ce1f49436 Mon Sep 17 00:00:00 2001 From: amer cerkic Date: Wed, 27 Feb 2019 15:31:23 -0800 Subject: [PATCH 47/87] manual pushing args to the app. Removed some debugging printouts --- .../questInterface/InterfaceActivity.java | 8 -------- .../io/highfidelity/oculus/OculusMobileActivity.java | 11 +++-------- 2 files changed, 3 insertions(+), 16 deletions(-) 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 index 119b009e6a..cdc48dfa37 100644 --- a/android/apps/questInterface/src/main/java/io/highfidelity/questInterface/InterfaceActivity.java +++ b/android/apps/questInterface/src/main/java/io/highfidelity/questInterface/InterfaceActivity.java @@ -14,14 +14,6 @@ public class InterfaceActivity extends OculusMobileActivity { @Override public void onCreate(Bundle savedInstanceState) { - if(this.getIntent().hasExtra("applicationArguments")){ - System.out.println("QQQ_ InterfaceActivity: args EXISTS"); - System.out.println("QQQ_ "+ this.getIntent().getStringExtra("applicationArguments")); - } - else{ - System.out.println("QQQ_ InterfaceActivity: NO argmument"); - } - HifiUtils.upackAssets(getAssets(), getCacheDir().getAbsolutePath()); super.onCreate(savedInstanceState); } 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 095236ecc3..75faf1e0dd 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 @@ -39,19 +39,14 @@ public class OculusMobileActivity extends QtActivity implements SurfaceHolder.Ca private SurfaceHolder mSurfaceHolder; public void onCreate(Bundle savedInstanceState) { - if(this.getIntent().hasExtra("applicationArguments")){ - System.out.println("QQQ_ OculusMobileActivity has arguments"); - System.out.println("QQQ_ "+ this.getIntent().getStringExtra("applicationArguments")); + if(getIntent().hasExtra("applicationArguments")){ + super.APPLICATION_PARAMETERS=getIntent().getStringExtra("applicationArguments"); } - else{ - System.out.println("QQQ_ OculusMobileActivity has NO arguments"); - } - - super.onCreate(savedInstanceState); + 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) From a425becc8a624ef215a94142b4cdf132673524e5 Mon Sep 17 00:00:00 2001 From: amer cerkic Date: Wed, 27 Feb 2019 15:35:33 -0800 Subject: [PATCH 48/87] clean up of debugging --- android/apps/questInterface/src/main/AndroidManifest.xml | 2 +- .../highfidelity/questInterface/PermissionsChecker.java | 6 ++++-- .../java/io/highfidelity/oculus/OculusMobileActivity.java | 1 - interface/src/Application.cpp | 8 ++++---- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/android/apps/questInterface/src/main/AndroidManifest.xml b/android/apps/questInterface/src/main/AndroidManifest.xml index 0f3616612a..a5de47bdce 100644 --- a/android/apps/questInterface/src/main/AndroidManifest.xml +++ b/android/apps/questInterface/src/main/AndroidManifest.xml @@ -28,7 +28,7 @@ android:excludeFromRecents="true"> - + ()->handleLookupString(QUEST_DEV); + const QString QUEST_DEV = "hifi://quest-dev"; + DependencyManager::get()->handleLookupString(QUEST_DEV); #endif } @@ -3669,8 +3669,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); From 95628dfc111e771842e60cd43ddf08ed70019005 Mon Sep 17 00:00:00 2001 From: amer cerkic Date: Wed, 27 Feb 2019 15:36:49 -0800 Subject: [PATCH 49/87] removing unused imports --- .../io/highfidelity/questInterface/InterfaceActivity.java | 6 ------ 1 file changed, 6 deletions(-) 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 index cdc48dfa37..df05576ea9 100644 --- a/android/apps/questInterface/src/main/java/io/highfidelity/questInterface/InterfaceActivity.java +++ b/android/apps/questInterface/src/main/java/io/highfidelity/questInterface/InterfaceActivity.java @@ -1,16 +1,10 @@ package io.highfidelity.questInterface; -import android.content.Intent; import android.os.Bundle; -import android.text.TextUtils; - import io.highfidelity.oculus.OculusMobileActivity; import io.highfidelity.utils.HifiUtils; public class InterfaceActivity extends OculusMobileActivity { - public static final String DOMAIN_URL = "url"; - private static final String TAG = "Interface"; - public static final String EXTRA_ARGS = "args"; @Override public void onCreate(Bundle savedInstanceState) { From de3c5e0ffeee801a75b4e91ed928730dcb0b0006 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Thu, 28 Feb 2019 17:51:58 +0100 Subject: [PATCH 50/87] relative URLs in FST material parsing --- libraries/model-baker/src/model-baker/Baker.cpp | 3 ++- .../src/model-baker/ParseMaterialMappingTask.cpp | 9 +++++---- .../src/model-baker/ParseMaterialMappingTask.h | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/libraries/model-baker/src/model-baker/Baker.cpp b/libraries/model-baker/src/model-baker/Baker.cpp index dfb18eef86..f47a9dcd62 100644 --- a/libraries/model-baker/src/model-baker/Baker.cpp +++ b/libraries/model-baker/src/model-baker/Baker.cpp @@ -155,7 +155,8 @@ namespace baker { const auto jointIndices = jointInfoOut.getN(2); // Parse material mapping - const auto materialMapping = model.addJob("ParseMaterialMapping", mapping); + const auto parseMaterialMappingInputs = ParseMaterialMappingTask::Input(url, mapping).asVarying(); + const auto materialMapping = model.addJob("ParseMaterialMapping", parseMaterialMappingInputs); // Combine the outputs into a new hfm::Model const auto buildBlendshapesInputs = BuildBlendshapesTask::Input(blendshapesPerMeshIn, normalsPerBlendshapePerMesh, tangentsPerBlendshapePerMesh).asVarying(); diff --git a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp index 7a923a3702..f8634e4170 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.get0(); + const auto& mapping = input.get1(); 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..8ad98edeb9 100644 --- a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h +++ b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h @@ -19,7 +19,7 @@ class ParseMaterialMappingTask { public: - using Input = QVariantHash; + using Input = baker::VaryingSet2 ; using Output = MaterialMapping; using JobModel = baker::Job::ModelIO; From faedc61c37909f8b38beff48cbe6f47a60aea174 Mon Sep 17 00:00:00 2001 From: amer cerkic Date: Thu, 28 Feb 2019 10:54:30 -0800 Subject: [PATCH 51/87] removing quest-demo specific changes --- android/apps/questInterface/build.gradle | 13 ++++--------- .../highfidelity/oculus/OculusMobileActivity.java | 6 +++--- interface/src/Application.cpp | 6 ------ libraries/animation/src/AnimInverseKinematics.cpp | 6 ------ .../src/OculusMobileDisplayPlugin.cpp | 14 +++++++------- 5 files changed, 14 insertions(+), 31 deletions(-) diff --git a/android/apps/questInterface/build.gradle b/android/apps/questInterface/build.gradle index bf600b5df1..43ce0c0c37 100644 --- a/android/apps/questInterface/build.gradle +++ b/android/apps/questInterface/build.gradle @@ -44,15 +44,10 @@ android { } signingConfigs { release { - // Hack for Quest demo - storeFile file("keystore.jks") - storePassword "password" - keyAlias "key0" - keyPassword "password" - // 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) : 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 : '' v2SigningEnabled false } } 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 125ed46ec9..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 @@ -108,14 +108,14 @@ public class OculusMobileActivity extends QtActivity implements SurfaceHolder.Ca @Override protected void onRestart() { super.onRestart(); - Log.w(TAG, "QQQ_ onRestart called ****"); + 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; } @@ -129,7 +129,7 @@ public class OculusMobileActivity extends QtActivity implements SurfaceHolder.Ca @Override public void surfaceDestroyed(SurfaceHolder holder) { - Log.w(TAG, "QQQ_ surfaceDestroyed ***************************************************"); + Log.w(TAG, "QQQ_ surfaceDestroyed"); nativeOnSurfaceChanged(null); mSurfaceHolder = null; } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 08785ecc90..c889377d6f 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2413,15 +2413,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo 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(); - -#if defined(Q_OS_ANDROID) - const QString QUEST_DEV = "hifi://quest-dev"; - DependencyManager::get()->handleLookupString(QUEST_DEV); -#endif } void Application::updateVerboseLogging() { diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index 37859c939a..9a55b66a39 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -865,12 +865,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/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp index a63b954f02..9809d02866 100644 --- a/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp +++ b/libraries/oculusMobilePlugin/src/OculusMobileDisplayPlugin.cpp @@ -58,7 +58,7 @@ void OculusMobileDisplayPlugin::deinit() { bool OculusMobileDisplayPlugin::internalActivate() { _renderTargetSize = { 1024, 512 }; - _cullingProjection = ovr::toGlm(ovrMatrix4f_CreateProjectionFov(90.0f, 90.0f, 90.0f, 90.0f, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP)); + _cullingProjection = ovr::toGlm(ovrMatrix4f_CreateProjectionFov(90.0f, 90.0f, 0.0f, 0.0f, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP)); withOvrJava([&](const ovrJava* java){ @@ -220,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); } From c1516df58dda201958bb2d5405284968f7fe6992 Mon Sep 17 00:00:00 2001 From: raveenajain Date: Thu, 28 Feb 2019 11:35:29 -0800 Subject: [PATCH 52/87] transforms using joints --- libraries/fbx/src/GLTFSerializer.cpp | 43 +++++++++++++--------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/libraries/fbx/src/GLTFSerializer.cpp b/libraries/fbx/src/GLTFSerializer.cpp index 736e7831c1..6dadea29d8 100755 --- a/libraries/fbx/src/GLTFSerializer.cpp +++ b/libraries/fbx/src/GLTFSerializer.cpp @@ -763,17 +763,23 @@ 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) { + joint.preTransform = glm::mat4(1); + for (int i = 0; i < node.transforms.size(); i++) { + joint.preTransform = node.transforms[i] * joint.preTransform; + } + joint.name = node.name; + hfmModel.joints[jointInd] = joint; + jointInd++; + } //Build materials QVector materialIDs; @@ -804,7 +810,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 +913,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 +963,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(); } From 15989e3a89bb10fe93e9dfe139132144001b6e66 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Thu, 28 Feb 2019 11:43:16 -0800 Subject: [PATCH 53/87] WIP. --- tools/nitpick/src/ImageComparer.cpp | 20 +++++++++++++++-- tools/nitpick/src/ImageComparer.h | 11 +++++++++- tools/nitpick/src/MismatchWindow.cpp | 32 ++++++++++++++++++++++++++++ tools/nitpick/src/MismatchWindow.h | 2 ++ tools/nitpick/src/Nitpick.cpp | 2 +- tools/nitpick/src/Test.cpp | 18 ++++++++++++---- tools/nitpick/src/Test.h | 4 ++-- tools/nitpick/src/common.h | 8 +++++++ 8 files changed, 87 insertions(+), 10 deletions(-) diff --git a/tools/nitpick/src/ImageComparer.cpp b/tools/nitpick/src/ImageComparer.cpp index fa73f97887..b9d556e544 100644 --- a/tools/nitpick/src/ImageComparer.cpp +++ b/tools/nitpick/src/ImageComparer.cpp @@ -12,9 +12,17 @@ #include +ImageComparer::ImageComparer() { + _ssimResults = new SSIMResults(); +} + +ImageComparer::~ImageComparer() { + delete _ssimResults; +} + // 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 { +double ImageComparer::compareImages(const QImage& resultImage, const QImage& expectedImage) const { const int L = 255; // (2^number of bits per pixel) - 1 const double K1 { 0.01 }; @@ -96,6 +104,7 @@ 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); + _ssimResults->results.push_back(numerator / denominator); ssim += numerator / denominator; ++windowCounter; @@ -106,5 +115,12 @@ double ImageComparer::compareImages(QImage resultImage, QImage expectedImage) co y = 0; } + _ssimResults->width = (int)(expectedImage.width() / WIN_SIZE); + _ssimResults->height = (int)(expectedImage.height() / WIN_SIZE); + return ssim / windowCounter; -}; \ No newline at end of file +}; + +SSIMResults* ImageComparer::getSSIMResults() { + return _ssimResults; +} diff --git a/tools/nitpick/src/ImageComparer.h b/tools/nitpick/src/ImageComparer.h index 7b7b8b0b74..5bea8151ec 100644 --- a/tools/nitpick/src/ImageComparer.h +++ b/tools/nitpick/src/ImageComparer.h @@ -10,12 +10,21 @@ #ifndef hifi_ImageComparer_h #define hifi_ImageComparer_h +#include "common.h" + #include #include class ImageComparer { public: - double compareImages(QImage resultImage, QImage expectedImage) const; + ImageComparer(); + ~ImageComparer(); + + double compareImages(const QImage& resultImage, const QImage& expectedImage) const; + 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..7649929551 100644 --- a/tools/nitpick/src/MismatchWindow.cpp +++ b/tools/nitpick/src/MismatchWindow.cpp @@ -99,3 +99,35 @@ void MismatchWindow::on_abortTestsButton_clicked() { QPixmap MismatchWindow::getComparisonImage() { return _diffPixmap; } + +QPixmap MismatchWindow::getSSIMResultsImage(SSIMResults* ssimResults) { + // This is an optimization, as QImage.setPixel() is embarrassingly slow + const int ELEMENT_SIZE { 8 }; + unsigned char* buffer = new unsigned char[(ssimResults->height * ELEMENT_SIZE) * (ssimResults->width * ELEMENT_SIZE ) * 3]; + + + // loop over each SSIM result (a double in [-1.0 .. 1.0] + int i { 0 }; + for (int y = 0; y < ssimResults->height; ++y) { + for (int x = 0; x < ssimResults->width; ++x) { + ////QRgb pixelP = expectedImage.pixel(QPoint(x, y)); + + ////// Convert to luminance + ////double p = R_Y * qRed(pixelP) + G_Y * qGreen(pixelP) + B_Y * qBlue(pixelP); + + ////// The intensity value is modified to increase the brightness of the displayed image + ////double absoluteDifference = fabs(p - q) / 255.0; + ////double modifiedDifference = sqrt(absoluteDifference); + + ////int difference = (int)(modifiedDifference * 255.0); + + ////buffer[3 * (x + y * expectedImage.width()) + 0] = difference; + ////buffer[3 * (x + y * expectedImage.width()) + 1] = difference; + ////buffer[3 * (x + y * expectedImage.width()) + 2] = difference; + + ++i; + } + } + + return QPixmap(); +} diff --git a/tools/nitpick/src/MismatchWindow.h b/tools/nitpick/src/MismatchWindow.h index 040e0b8bf1..d80e371ba0 100644 --- a/tools/nitpick/src/MismatchWindow.h +++ b/tools/nitpick/src/MismatchWindow.h @@ -25,7 +25,9 @@ public: UserResponse getUserResponse() { return _userResponse; } QPixmap computeDiffPixmap(QImage expectedImage, QImage resultImage); + QPixmap getComparisonImage(); + QPixmap getSSIMResultsImage(SSIMResults* ssimResults); private slots: void on_passTestButton_clicked(); diff --git a/tools/nitpick/src/Nitpick.cpp b/tools/nitpick/src/Nitpick.cpp index 39800c6bc6..e17ea94634 100644 --- a/tools/nitpick/src/Nitpick.cpp +++ b/tools/nitpick/src/Nitpick.cpp @@ -40,7 +40,7 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) { _ui.plainTextEdit->setReadOnly(true); - setWindowTitle("Nitpick - v3.0.0"); + setWindowTitle("Nitpick - v3.0.1"); clientProfiles << "VR-High" << "Desktop-High" << "Desktop-Low" << "Mobile-Touch" << "VR-Standalone"; _ui.clientProfileComboBox->insertItems(0, clientProfiles); diff --git a/tools/nitpick/src/Test.cpp b/tools/nitpick/src/Test.cpp index e8e284bf32..a1f1bf92eb 100644 --- a/tools/nitpick/src/Test.cpp +++ b/tools/nitpick/src/Test.cpp @@ -100,12 +100,14 @@ int Test::compareImageLists() { }; _mismatchWindow.setTestResult(testResult); + + QPixmap ssimResultsPixMap = _mismatchWindow.getSSIMResultsImage(_imageComparer.getSSIMResults()); if (similarityIndex < THRESHOLD) { ++numberOfFailures; if (!isInteractiveMode) { - appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), true); + appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), ssimResultsPixMap, true); } else { _mismatchWindow.exec(); @@ -113,7 +115,7 @@ int Test::compareImageLists() { case USER_RESPONSE_PASS: break; case USE_RESPONSE_FAIL: - appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), true); + appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), ssimResultsPixMap, true); break; case USER_RESPONSE_ABORT: keepOn = false; @@ -124,7 +126,7 @@ int Test::compareImageLists() { } } } else { - appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), false); + appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), ssimResultsPixMap, false); } _progressBar->setValue(i); @@ -156,7 +158,7 @@ int Test::checkTextResults() { return testsFailed.length(); } -void Test::appendTestResultsToFile(TestResult testResult, QPixmap comparisonImage, bool hasFailed) { +void Test::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"); @@ -216,6 +218,14 @@ void Test::appendTestResultsToFile(TestResult testResult, QPixmap comparisonImag exit(-1); } + // Create the SSIM results image + sourceFile = testResult._pathname + testResult._actualImageFilename; + destinationFile = resultFolderPath + "/" + "SSIM results.png"; + if (!QFile::copy(sourceFile, destinationFile)) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Failed to copy " + sourceFile + " to " + destinationFile); + exit(-1); + } + comparisonImage.save(resultFolderPath + "/" + "Difference Image.png"); } diff --git a/tools/nitpick/src/Test.h b/tools/nitpick/src/Test.h index 23011d0c31..752dcbc5af 100644 --- a/tools/nitpick/src/Test.h +++ b/tools/nitpick/src/Test.h @@ -87,7 +87,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); @@ -116,7 +116,7 @@ private: const QString TEST_RESULTS_FOLDER { "TestResults" }; const QString TEST_RESULTS_FILENAME { "TestResults.txt" }; - const double THRESHOLD{ 0.935 }; + const double THRESHOLD{ 0.98 }; QDir _imageDirectory; diff --git a/tools/nitpick/src/common.h b/tools/nitpick/src/common.h index 5df4e9c921..ac776995b7 100644 --- a/tools/nitpick/src/common.h +++ b/tools/nitpick/src/common.h @@ -10,6 +10,7 @@ #ifndef hifi_common_h #define hifi_common_h +#include #include class TestResult { @@ -39,4 +40,11 @@ const double R_Y = 0.212655f; const double G_Y = 0.715158f; const double B_Y = 0.072187f; +class SSIMResults { +public: + int width; + int height; + std::vector results; +}; + #endif // hifi_common_h \ No newline at end of file From a72b02cd25989f7d11af0cf2dd4a511de6b77bde Mon Sep 17 00:00:00 2001 From: r3tk0n Date: Thu, 28 Feb 2019 13:08:05 -0800 Subject: [PATCH 54/87] Clean up code and add comments. --- .../controllers/controllerModules/farGrabEntity.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/system/controllers/controllerModules/farGrabEntity.js b/scripts/system/controllers/controllerModules/farGrabEntity.js index 5e621809b2..65a3671cae 100644 --- a/scripts/system/controllers/controllerModules/farGrabEntity.js +++ b/scripts/system/controllers/controllerModules/farGrabEntity.js @@ -83,14 +83,11 @@ Script.include("/~/system/libraries/controllers.js"); 100, makeLaserParams(this.hand, false)); - //enableDispatcherModule("LeftFarGrabEntity", leftFarGrabEntity); - //enableDispatcherModule("RightFarGrabEntity", rightFarGrabEntity); - this.getOtherModule = function () { - // Used to fetch other module. return getEnabledModuleByName(this.hand === RIGHT_HAND ? ("LeftFarGrabEntity") : ("RightFarGrabEntity")); }; + // Get the rotation of the fargrabbed entity. this.getTargetRotation = function () { if (this.targetIsNull()) { return null; @@ -108,10 +105,12 @@ Script.include("/~/system/libraries/controllers.js"); 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)); }; @@ -303,11 +302,14 @@ Script.include("/~/system/libraries/controllers.js"); 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); From 6aede024f40a23eada9ea5e2a07a613f84b3f7e4 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Thu, 28 Feb 2019 23:22:55 +0100 Subject: [PATCH 55/87] use fst mapping path as reference instead of model path --- .../model-baker/src/model-baker/Baker.cpp | 7 ++-- libraries/model-baker/src/model-baker/Baker.h | 3 +- .../model-baker/src/model-baker/BakerTypes.h | 2 ++ .../model-baker/ParseMaterialMappingTask.cpp | 4 +-- .../model-baker/ParseMaterialMappingTask.h | 3 +- .../src/model-baker/PrepareJointsTask.cpp | 4 +-- .../src/model-baker/PrepareJointsTask.h | 3 +- .../src/model-networking/ModelCache.cpp | 34 ++++++++++--------- .../src/model-networking/ModelCache.h | 9 +++-- 9 files changed, 40 insertions(+), 29 deletions(-) diff --git a/libraries/model-baker/src/model-baker/Baker.cpp b/libraries/model-baker/src/model-baker/Baker.cpp index f47a9dcd62..c0c473315d 100644 --- a/libraries/model-baker/src/model-baker/Baker.cpp +++ b/libraries/model-baker/src/model-baker/Baker.cpp @@ -117,7 +117,7 @@ namespace baker { 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) { @@ -155,8 +155,7 @@ namespace baker { const auto jointIndices = jointInfoOut.getN(2); // Parse material mapping - const auto parseMaterialMappingInputs = ParseMaterialMappingTask::Input(url, mapping).asVarying(); - const auto materialMapping = model.addJob("ParseMaterialMapping", parseMaterialMappingInputs); + const auto materialMapping = model.addJob("ParseMaterialMapping", mapping); // Combine the outputs into a new hfm::Model const auto buildBlendshapesInputs = BuildBlendshapesTask::Input(blendshapesPerMeshIn, normalsPerBlendshapePerMesh, tangentsPerBlendshapePerMesh).asVarying(); @@ -170,7 +169,7 @@ namespace baker { } }; - 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/ParseMaterialMappingTask.cpp b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp index f8634e4170..0a1964d8cd 100644 --- a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp +++ b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.cpp @@ -11,8 +11,8 @@ #include "ModelBakerLogging.h" void ParseMaterialMappingTask::run(const baker::BakeContextPointer& context, const Input& input, Output& output) { - const auto& url = input.get0(); - const auto& mapping = input.get1(); + const auto& url = input.first; + const auto& mapping = input.second; MaterialMapping materialMapping; auto mappingIter = mapping.find("materialMap"); diff --git a/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h b/libraries/model-baker/src/model-baker/ParseMaterialMappingTask.h index 8ad98edeb9..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 = baker::VaryingSet2 ; + 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 581196b2cc..8852e2a262 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,7 @@ 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.second, geometryExtra.textureBaseUrl, geometryExtra.combineParts); return result; } }; @@ -151,7 +153,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 +193,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 +206,7 @@ private: ModelLoader _modelLoader; QWeakPointer _resource; QUrl _url; - QVariantHash _mapping; + GeometryMappingPair _mapping; QByteArray _data; bool _combineParts; QString _webMediaType; @@ -244,7 +246,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 +272,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) { @@ -312,17 +313,17 @@ public: 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) { 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 +336,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(); @@ -398,7 +399,7 @@ QSharedPointer ModelCache::createResourceCopy(const QSharedPointer()(geometryExtra)).staticCast(); @@ -411,7 +412,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: From ff8cb27256bea4a9c82bdc43e1088592130b557f Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Thu, 28 Feb 2019 23:45:08 +0100 Subject: [PATCH 56/87] include fst URL in the GeometryExtra hash --- libraries/model-networking/src/model-networking/ModelCache.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index 8852e2a262..9e0df4a3c7 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -80,7 +80,8 @@ namespace std { struct hash { size_t operator()(const GeometryExtra& geometryExtra) const { size_t result = 0; - hash_combine(result, geometryExtra.mapping.second, geometryExtra.textureBaseUrl, geometryExtra.combineParts); + hash_combine(result, geometryExtra.mapping.first, geometryExtra.mapping.second, geometryExtra.textureBaseUrl, + geometryExtra.combineParts); return result; } }; From 1476251dcb359f335084e4413f6f4c1634085a7f Mon Sep 17 00:00:00 2001 From: r3tk0n Date: Thu, 28 Feb 2019 15:21:24 -0800 Subject: [PATCH 57/87] Enable bubble on AFK, respect user setting upon return from AFK. --- scripts/system/away.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/scripts/system/away.js b/scripts/system/away.js index 45b6f43b73..3deb2249be 100644 --- a/scripts/system/away.js +++ b/scripts/system/away.js @@ -65,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; @@ -166,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; } @@ -179,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(); } From b26b02fed6d40c5acd75ccc3a4c50fea03738198 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 28 Feb 2019 16:59:31 -0800 Subject: [PATCH 58/87] potential 2d overlay threading fixes --- interface/src/ui/overlays/Overlay.cpp | 19 +--- interface/src/ui/overlays/Overlay.h | 9 +- interface/src/ui/overlays/Overlay2D.cpp | 22 +--- interface/src/ui/overlays/Overlay2D.h | 5 - interface/src/ui/overlays/Overlays.cpp | 133 +++++++++++------------ interface/src/ui/overlays/QmlOverlay.cpp | 28 ++--- interface/src/ui/overlays/QmlOverlay.h | 4 +- 7 files changed, 77 insertions(+), 143 deletions(-) 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 9b2f741531..8fd5d236a0 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -772,29 +772,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); @@ -835,15 +835,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(); })); } @@ -919,6 +918,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; @@ -933,15 +937,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(); } @@ -949,15 +952,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)); } @@ -969,6 +971,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; @@ -976,8 +984,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(); @@ -991,9 +998,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(); } @@ -1009,12 +1014,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)); @@ -1141,38 +1142,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) { 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(); From 9fe0309ae65d0ae2d42a48e43812935ebab6a752 Mon Sep 17 00:00:00 2001 From: danteruiz Date: Thu, 28 Feb 2019 17:11:48 -0800 Subject: [PATCH 59/87] another login fix --- interface/src/Application.cpp | 8 +++++--- interface/src/Application.h | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index ebc1176ee1..274d6919af 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4980,10 +4980,11 @@ void Application::idle() { } { - if (_keyboardFocusWaitingOnRenderable && getEntities()->renderableForEntityId(_keyboardFocusedEntity.get())) { - _keyboardFocusWaitingOnRenderable = false; - QUuid entityId = _keyboardFocusedEntity.get(); + if (_keyboardFocusWaitingOnRenderable && getEntities()->renderableForEntityId(_entityIdWaitingOnRenderable.get())) { + QUuid entityId = _entityIdWaitingOnRenderable.get(); + _entityIdWaitingOnRenderable.set(UNKNOWN_ENTITY_ID); setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); + _keyboardFocusWaitingOnRenderable = false; setKeyboardFocusEntity(entityId); } } @@ -5836,6 +5837,7 @@ void Application::setKeyboardFocusEntity(const QUuid& id) { auto entityItemRenderable = entities->renderableForEntityId(entityId); if (!entityItemRenderable) { _keyboardFocusWaitingOnRenderable = true; + _entityIdWaitingOnRenderable.set(id); } else if (entityItemRenderable->wantsKeyboardFocus()) { entities->setProxyWindow(entityId, _window->windowHandle()); if (_keyboardMouseDevice->isActive()) { diff --git a/interface/src/Application.h b/interface/src/Application.h index c16f260192..5e3090973a 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -733,6 +733,7 @@ private: bool _reticleClickPressed { false }; bool _keyboardFocusWaitingOnRenderable { false }; + ThreadSafeValueCache _entityIdWaitingOnRenderable; int _avatarAttachmentRequest = 0; From 9a2bd87278e964f8623d7c5f2f7fdfde1e441834 Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Thu, 28 Feb 2019 18:02:07 -0800 Subject: [PATCH 60/87] Fix for case when animated joints are missing from the target avatar skeleton By copying the animation rotations over to the target avatar in absolute frame, rather then relative, we can properly "combine" animated rotations that aren't in the target avatar skeleton. --- libraries/animation/src/AnimClip.cpp | 99 ++++++++++++++++-------- libraries/animation/src/AnimSkeleton.cpp | 11 +++ libraries/animation/src/AnimSkeleton.h | 1 + 3 files changed, 78 insertions(+), 33 deletions(-) diff --git a/libraries/animation/src/AnimClip.cpp b/libraries/animation/src/AnimClip.cpp index a35e0237d0..18e97cf0a1 100644 --- a/libraries/animation/src/AnimClip.cpp +++ b/libraries/animation/src/AnimClip.cpp @@ -95,57 +95,85 @@ 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(); auto avatarSkeleton = getSkeleton(); - - // build a mapping from animation joint indices to avatar joint indices. - // by matching joints with the same name. const HFMModel& animModel = _networkAnim->getHFMModel(); AnimSkeleton animSkeleton(animModel); const int animJointCount = animSkeleton.getNumJoints(); const int avatarJointCount = avatarSkeleton->getNumJoints(); - std::vector animToAvatarJointIndexMap; - animToAvatarJointIndexMap.reserve(animJointCount); - for (int animJointIndex = 0; animJointIndex < animJointCount; animJointIndex++) { - QString animJointName = animSkeleton.getJointName(animJointIndex); - int avatarJointIndex = avatarSkeleton->nameToJointIndex(animJointName); - animToAvatarJointIndexMap.push_back(avatarJointIndex); - } + + // 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); for (int frame = 0; frame < animFrameCount; frame++) { - const HFMAnimationFrame& animFrame = animModel.animationFrames[frame]; - // init all joints in animation to default pose - // this will give us a resonable result for bones in the avatar skeleton but not in the animation. - _anim[frame].reserve(avatarJointCount); - for (int avatarJointIndex = 0; avatarJointIndex < avatarJointCount; avatarJointIndex++) { - _anim[frame].push_back(avatarSkeleton->getRelativeDefaultPose(avatarJointIndex)); + // 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 animJointIndex = 0; animJointIndex < animJointCount; animJointIndex++) { - int avatarJointIndex = animToAvatarJointIndexMap[animJointIndex]; + // convert rotations into absolute frame + animSkeleton.convertRelativeRotationsToAbsolute(animRotations); - // skip joints that are in the animation but not in the avatar. - if (avatarJointIndex >= 0 && avatarJointIndex < avatarJointCount) { + // 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]; + } + 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]; - const glm::quat& animRot = animFrame.rotations[animJointIndex]; - - const AnimPose& animPreRotPose = animSkeleton.getPreRotationPose(animJointIndex); - AnimPose animPostRotPose = animSkeleton.getPostRotationPose(animJointIndex); - AnimPose animRotPose(glm::vec3(1.0f), animRot, glm::vec3()); - - // adjust anim scale to equal the scale from the avatar joint. - // we do not support animated scale. - const AnimPose& avatarDefaultPose = avatarSkeleton->getRelativeDefaultPose(avatarJointIndex); - animPostRotPose.scale() = avatarDefaultPose.scale(); // retarget translation from animation to avatar const glm::vec3& animZeroTrans = animModel.animationFrames[0].translations[animJointIndex]; @@ -154,10 +182,15 @@ void AnimClip::copyFromNetworkAnim() { if (fabsf(glm::length(animZeroTrans)) > EPSILON) { boneLengthScale = glm::length(avatarDefaultPose.trans()) / glm::length(animZeroTrans); } - AnimPose animTransPose = AnimPose(glm::vec3(1.0f), glm::quat(), avatarDefaultPose.trans() + boneLengthScale * (animTrans - animZeroTrans)); - - _anim[frame][avatarJointIndex] = animTransPose * animPreRotPose * animRotPose * animPostRotPose; + 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)); } } diff --git a/libraries/animation/src/AnimSkeleton.cpp b/libraries/animation/src/AnimSkeleton.cpp index 03e3ac6ebd..993a2c6632 100644 --- a/libraries/animation/src/AnimSkeleton.cpp +++ b/libraries/animation/src/AnimSkeleton.cpp @@ -149,6 +149,17 @@ void AnimSkeleton::convertAbsolutePosesToRelative(AnimPoseVec& poses) const { } } +void AnimSkeleton::convertRelativeRotationsToAbsolute(std::vector& rotations) const { + // poses 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 int lastIndex = std::min((int)rotations.size(), _jointsSize); diff --git a/libraries/animation/src/AnimSkeleton.h b/libraries/animation/src/AnimSkeleton.h index 0eefbf973e..5309d26354 100644 --- a/libraries/animation/src/AnimSkeleton.h +++ b/libraries/animation/src/AnimSkeleton.h @@ -54,6 +54,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; From 7bcf727a835cc446b1f917a130f80af45c1ddfd4 Mon Sep 17 00:00:00 2001 From: danteruiz Date: Thu, 28 Feb 2019 18:11:51 -0800 Subject: [PATCH 61/87] review changes --- interface/src/Application.cpp | 6 ++---- interface/src/Application.h | 1 - 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 274d6919af..248ada4376 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4980,9 +4980,8 @@ void Application::idle() { } { - if (_keyboardFocusWaitingOnRenderable && getEntities()->renderableForEntityId(_entityIdWaitingOnRenderable.get())) { - QUuid entityId = _entityIdWaitingOnRenderable.get(); - _entityIdWaitingOnRenderable.set(UNKNOWN_ENTITY_ID); + if (_keyboardFocusWaitingOnRenderable && getEntities()->renderableForEntityId(_keyboardFocusedEntity.get())) { + QUuid entityId = _keyboardFocusedEntity.get(); setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); _keyboardFocusWaitingOnRenderable = false; setKeyboardFocusEntity(entityId); @@ -5837,7 +5836,6 @@ void Application::setKeyboardFocusEntity(const QUuid& id) { auto entityItemRenderable = entities->renderableForEntityId(entityId); if (!entityItemRenderable) { _keyboardFocusWaitingOnRenderable = true; - _entityIdWaitingOnRenderable.set(id); } else if (entityItemRenderable->wantsKeyboardFocus()) { entities->setProxyWindow(entityId, _window->windowHandle()); if (_keyboardMouseDevice->isActive()) { diff --git a/interface/src/Application.h b/interface/src/Application.h index 5e3090973a..c16f260192 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -733,7 +733,6 @@ private: bool _reticleClickPressed { false }; bool _keyboardFocusWaitingOnRenderable { false }; - ThreadSafeValueCache _entityIdWaitingOnRenderable; int _avatarAttachmentRequest = 0; From d4fa1e25fb0d4be161de1735c7368e9063fd18c1 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Thu, 28 Feb 2019 20:33:21 -0800 Subject: [PATCH 62/87] Working. --- tools/nitpick/src/AWSInterface.cpp | 33 ++++++++++++------ tools/nitpick/src/AWSInterface.h | 6 ++++ tools/nitpick/src/ImageComparer.cpp | 39 ++++++++++++--------- tools/nitpick/src/ImageComparer.h | 9 +++-- tools/nitpick/src/MismatchWindow.cpp | 51 ++++++++++++++-------------- tools/nitpick/src/MismatchWindow.h | 6 ++-- tools/nitpick/src/Nitpick.cpp | 10 +++--- tools/nitpick/src/Nitpick.h | 3 +- tools/nitpick/src/Test.cpp | 41 ++++++++++++---------- tools/nitpick/src/Test.h | 6 +++- tools/nitpick/src/common.h | 27 ++++++++++----- tools/nitpick/ui/Nitpick.ui | 42 ++++++++++++++++++++--- 12 files changed, 175 insertions(+), 98 deletions(-) diff --git a/tools/nitpick/src/AWSInterface.cpp b/tools/nitpick/src/AWSInterface.cpp index 4e83460b9e..6dcc255286 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"; } @@ -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"; } } } 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/ImageComparer.cpp b/tools/nitpick/src/ImageComparer.cpp index b9d556e544..7e3e6eaf63 100644 --- a/tools/nitpick/src/ImageComparer.cpp +++ b/tools/nitpick/src/ImageComparer.cpp @@ -12,17 +12,9 @@ #include -ImageComparer::ImageComparer() { - _ssimResults = new SSIMResults(); -} - -ImageComparer::~ImageComparer() { - delete _ssimResults; -} - // 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(const QImage& resultImage, const 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 }; @@ -47,8 +39,13 @@ double ImageComparer::compareImages(const QImage& resultImage, const QImage& exp 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) { @@ -104,8 +101,13 @@ double ImageComparer::compareImages(const QImage& resultImage, const QImage& exp double numerator = (2.0 * mP * mQ + c1) * (2.0 * sigPQ + c2); double denominator = (mP * mP + mQ * mQ + c1) * (sigsqP + sigsqQ + c2); - _ssimResults->results.push_back(numerator / denominator); - 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; @@ -115,12 +117,17 @@ double ImageComparer::compareImages(const QImage& resultImage, const QImage& exp y = 0; } - _ssimResults->width = (int)(expectedImage.width() / WIN_SIZE); - _ssimResults->height = (int)(expectedImage.height() / WIN_SIZE); - - return ssim / windowCounter; + _ssimResults.width = (int)(expectedImage.width() / WIN_SIZE); + _ssimResults.height = (int)(expectedImage.height() / WIN_SIZE); + _ssimResults.min = min; + _ssimResults.max = max; + _ssimResults.ssim = ssim / windowCounter; }; -SSIMResults* ImageComparer::getSSIMResults() { +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 5bea8151ec..fc14dab94d 100644 --- a/tools/nitpick/src/ImageComparer.h +++ b/tools/nitpick/src/ImageComparer.h @@ -17,14 +17,13 @@ class ImageComparer { public: - ImageComparer(); - ~ImageComparer(); + void compareImages(const QImage& resultImage, const QImage& expectedImage); + double getSSIMValue(); - double compareImages(const QImage& resultImage, const QImage& expectedImage) const; - SSIMResults* getSSIMResults(); + SSIMResults getSSIMResults(); private: - SSIMResults* _ssimResults; + SSIMResults _ssimResults; }; #endif // hifi_ImageComparer_h diff --git a/tools/nitpick/src/MismatchWindow.cpp b/tools/nitpick/src/MismatchWindow.cpp index 7649929551..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); @@ -100,34 +100,35 @@ QPixmap MismatchWindow::getComparisonImage() { return _diffPixmap; } -QPixmap MismatchWindow::getSSIMResultsImage(SSIMResults* ssimResults) { +QPixmap MismatchWindow::getSSIMResultsImage(const SSIMResults& ssimResults) { // This is an optimization, as QImage.setPixel() is embarrassingly slow const int ELEMENT_SIZE { 8 }; - unsigned char* buffer = new unsigned char[(ssimResults->height * ELEMENT_SIZE) * (ssimResults->width * ELEMENT_SIZE ) * 3]; + 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 (a double in [-1.0 .. 1.0] - int i { 0 }; - for (int y = 0; y < ssimResults->height; ++y) { - for (int x = 0; x < ssimResults->width; ++x) { - ////QRgb pixelP = expectedImage.pixel(QPoint(x, y)); - - ////// Convert to luminance - ////double p = R_Y * qRed(pixelP) + G_Y * qGreen(pixelP) + B_Y * qBlue(pixelP); - - ////// The intensity value is modified to increase the brightness of the displayed image - ////double absoluteDifference = fabs(p - q) / 255.0; - ////double modifiedDifference = sqrt(absoluteDifference); - - ////int difference = (int)(modifiedDifference * 255.0); - - ////buffer[3 * (x + y * expectedImage.width()) + 0] = difference; - ////buffer[3 * (x + y * expectedImage.width()) + 1] = difference; - ////buffer[3 * (x + y * expectedImage.width()) + 2] = difference; - - ++i; + // 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 + } + } } } - return QPixmap(); + 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 d80e371ba0..116d35dfc5 100644 --- a/tools/nitpick/src/MismatchWindow.h +++ b/tools/nitpick/src/MismatchWindow.h @@ -20,14 +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(SSIMResults* ssimResults); + 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 e17ea94634..c07a76fc58 100644 --- a/tools/nitpick/src/Nitpick.cpp +++ b/tools/nitpick/src/Nitpick.cpp @@ -148,10 +148,6 @@ void Nitpick::on_tabWidget_currentChanged(int index) { } } -void Nitpick::on_evaluateTestsPushbutton_clicked() { - _test->startTestsEvaluation(false, false); -} - void Nitpick::on_createRecursiveScriptPushbutton_clicked() { _test->createRecursiveScript(); } @@ -242,6 +238,10 @@ void Nitpick::on_showTaskbarPushbutton_clicked() { #endif } +void Nitpick::on_evaluateTestsPushbutton_clicked() { + _test->startTestsEvaluation(false, false); +} + void Nitpick::on_closePushbutton_clicked() { exit(0); } @@ -255,7 +255,7 @@ void Nitpick::on_createXMLScriptRadioButton_clicked() { } void Nitpick::on_createWebPagePushbutton_clicked() { - _test->createWebPage(_ui.updateAWSCheckBox, _ui.awsURLLineEdit); + _test->createWebPage(_ui.updateAWSCheckBox, _ui.diffImageRadioButton, _ui.ssimImageRadioButton, _ui.awsURLLineEdit); } void Nitpick::downloadFile(const QUrl& url) { diff --git a/tools/nitpick/src/Nitpick.h b/tools/nitpick/src/Nitpick.h index 80fef934d6..3095a14c05 100644 --- a/tools/nitpick/src/Nitpick.h +++ b/tools/nitpick/src/Nitpick.h @@ -56,7 +56,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(); @@ -82,6 +81,8 @@ private slots: void on_hideTaskbarPushbutton_clicked(); void on_showTaskbarPushbutton_clicked(); + void on_evaluateTestsPushbutton_clicked(); + void on_createPythonScriptRadioButton_clicked(); void on_createXMLScriptRadioButton_clicked(); diff --git a/tools/nitpick/src/Test.cpp b/tools/nitpick/src/Test.cpp index a1f1bf92eb..7269fb3f02 100644 --- a/tools/nitpick/src/Test.cpp +++ b/tools/nitpick/src/Test.cpp @@ -89,25 +89,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); - QPixmap ssimResultsPixMap = _mismatchWindow.getSSIMResultsImage(_imageComparer.getSSIMResults()); - if (similarityIndex < THRESHOLD) { ++numberOfFailures; if (!isInteractiveMode) { - appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), ssimResultsPixMap, true); + appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), _mismatchWindow.getSSIMResultsImage(testResult._ssimResults), true); } else { _mismatchWindow.exec(); @@ -115,7 +115,7 @@ int Test::compareImageLists() { case USER_RESPONSE_PASS: break; case USE_RESPONSE_FAIL: - appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), ssimResultsPixMap, true); + appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), _mismatchWindow.getSSIMResultsImage(testResult._ssimResults), true); break; case USER_RESPONSE_ABORT: keepOn = false; @@ -126,7 +126,7 @@ int Test::compareImageLists() { } } } else { - appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), ssimResultsPixMap, false); + appendTestResultsToFile(testResult, _mismatchWindow.getComparisonImage(), _mismatchWindow.getSSIMResultsImage(testResult._ssimResults), false); } _progressBar->setValue(i); @@ -218,15 +218,10 @@ void Test::appendTestResultsToFile(const TestResult& testResult, const QPixmap& exit(-1); } - // Create the SSIM results image - sourceFile = testResult._pathname + testResult._actualImageFilename; - destinationFile = resultFolderPath + "/" + "SSIM results.png"; - if (!QFile::copy(sourceFile, destinationFile)) { - QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Failed to copy " + sourceFile + " to " + destinationFile); - exit(-1); - } - comparisonImage.save(resultFolderPath + "/" + "Difference Image.png"); + + // Save the SSIM results image + ssimResultsImage.save(resultFolderPath + "/" + "SSIM Image.png"); } void::Test::appendTestResultsToFile(QString testResultFilename, bool hasFailed) { @@ -1105,7 +1100,12 @@ void Test::setTestRailCreateMode(TestRailCreateMode testRailCreateMode) { _testRailCreateMode = testRailCreateMode; } -void Test::createWebPage(QCheckBox* updateAWSCheckBox, QLineEdit* urlLineEdit) { +void Test::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)"); if (testResults.isNull()) { @@ -1122,5 +1122,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/Test.h index 752dcbc5af..8753b9fcda 100644 --- a/tools/nitpick/src/Test.h +++ b/tools/nitpick/src/Test.h @@ -102,7 +102,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; diff --git a/tools/nitpick/src/common.h b/tools/nitpick/src/common.h index ac776995b7..eb228ff2b3 100644 --- a/tools/nitpick/src/common.h +++ b/tools/nitpick/src/common.h @@ -13,19 +13,35 @@ #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 { @@ -40,11 +56,4 @@ const double R_Y = 0.212655f; const double G_Y = 0.715158f; const double B_Y = 0.072187f; -class SSIMResults { -public: - int width; - int height; - std::vector results; -}; - #endif // hifi_common_h \ No newline at end of file diff --git a/tools/nitpick/ui/Nitpick.ui b/tools/nitpick/ui/Nitpick.ui index 47471522db..1857a2118f 100644 --- a/tools/nitpick/ui/Nitpick.ui +++ b/tools/nitpick/ui/Nitpick.ui @@ -43,7 +43,7 @@ - 0 + 5 @@ -760,7 +760,7 @@ 190 - 180 + 200 131 20 @@ -776,7 +776,7 @@ 330 - 170 + 190 181 51 @@ -889,8 +889,8 @@ - 270 - 30 + 370 + 20 160 51 @@ -922,6 +922,38 @@ + + + + 260 + 50 + 95 + 20 + + + + Diff Image + + + false + + + + + + 260 + 30 + 95 + 20 + + + + SSIM Image + + + true + + groupBox updateTestRailRunResultsPushbutton From cfb84967d55748d2c24989149dfbb427f809a0f6 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 1 Mar 2019 10:54:44 -0800 Subject: [PATCH 63/87] these seemed dangerous --- .../ui/src/ui/TabletScriptingInterface.cpp | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) 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(); } From ad69611b7cd1d9c1e1ae29204cdeee74b91d772e Mon Sep 17 00:00:00 2001 From: amer cerkic Date: Fri, 1 Mar 2019 11:08:00 -0800 Subject: [PATCH 64/87] removing redef of the two functions in avatar.h --- libraries/avatars-renderer/src/avatars-renderer/Avatar.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 9f713637f5..6c31f9fc93 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -287,8 +287,6 @@ public: * @returns {Quat} */ Q_INVOKABLE glm::quat jointToWorldRotation(const glm::quat& rotation, const int jointIndex = -1) const; - virtual glm::vec3 getSpine2SplineOffset() const { return _spine2SplineOffset; } - virtual float getSpine2SplineRatio() const { return _spine2SplineRatio; } virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override; virtual void setAttachmentData(const QVector& attachmentData) override; From 82b2050229bb0ad42d9ab3a16106fe0654ea4292 Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Fri, 1 Mar 2019 11:13:56 -0800 Subject: [PATCH 65/87] code review feedback --- libraries/animation/src/AnimSkeleton.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/animation/src/AnimSkeleton.cpp b/libraries/animation/src/AnimSkeleton.cpp index 993a2c6632..0f1c18c9dc 100644 --- a/libraries/animation/src/AnimSkeleton.cpp +++ b/libraries/animation/src/AnimSkeleton.cpp @@ -150,7 +150,7 @@ void AnimSkeleton::convertAbsolutePosesToRelative(AnimPoseVec& poses) const { } void AnimSkeleton::convertRelativeRotationsToAbsolute(std::vector& rotations) const { - // poses start off relative and leave in absolute frame + // 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]; @@ -161,7 +161,7 @@ void AnimSkeleton::convertRelativeRotationsToAbsolute(std::vector& ro } 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]; From dfb17c922bc783182dc3242097534f840ed5bb5f Mon Sep 17 00:00:00 2001 From: raveenajain Date: Fri, 1 Mar 2019 11:21:29 -0800 Subject: [PATCH 66/87] fix model not loading in bounding box --- libraries/fbx/src/GLTFSerializer.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/libraries/fbx/src/GLTFSerializer.cpp b/libraries/fbx/src/GLTFSerializer.cpp index 6dadea29d8..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++; } @@ -772,8 +774,10 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const QUrl& url) { 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 < node.transforms.size(); i++) { + for (int i = 0; i < size; i++) { joint.preTransform = node.transforms[i] * joint.preTransform; } joint.name = node.name; From 217145f4c562c78785715d8068cd9179cd162b93 Mon Sep 17 00:00:00 2001 From: amantley Date: Fri, 22 Feb 2019 17:16:50 -0800 Subject: [PATCH 67/87] This pr addresses two issues related to avatars that have parents joints above their hip joints. First on the IK side this prevents parent joints from being included in the accumulators in AnimInverseKinematics. Second in AnimClip the boneLengthScale now takes into account translation and scale on these extra parent joints. --- libraries/animation/src/AnimClip.cpp | 41 ++++++++++++++++++- .../animation/src/AnimInverseKinematics.cpp | 6 +-- libraries/animation/src/AnimSkeleton.cpp | 3 ++ libraries/animation/src/AnimSkeleton.h | 2 + 4 files changed, 46 insertions(+), 6 deletions(-) diff --git a/libraries/animation/src/AnimClip.cpp b/libraries/animation/src/AnimClip.cpp index a35e0237d0..109be27b58 100644 --- a/libraries/animation/src/AnimClip.cpp +++ b/libraries/animation/src/AnimClip.cpp @@ -151,12 +151,49 @@ void AnimClip::copyFromNetworkAnim() { const glm::vec3& animZeroTrans = animModel.animationFrames[0].translations[animJointIndex]; float boneLengthScale = 1.0f; const float EPSILON = 0.0001f; - if (fabsf(glm::length(animZeroTrans)) > EPSILON) { - boneLengthScale = glm::length(avatarDefaultPose.trans()) / glm::length(animZeroTrans); + + const int avatarHipsParentIndex = avatarSkeleton->getParentIndex(avatarSkeleton->nameToJointIndex("Hips")); + if (avatarJointIndex == avatarSkeleton->nameToJointIndex("Hips") && (avatarHipsParentIndex >= 0)) { + + const AnimPose& animHipsAbsoluteDefaultPose = animSkeleton.getAbsoluteDefaultPose(animSkeleton.nameToJointIndex("Hips")); + const AnimPose& avatarHipsAbsoluteDefaultPose = avatarSkeleton->getAbsoluteDefaultPose(avatarSkeleton->nameToJointIndex("Hips")); + const AnimPose& avatarHipsParentAbsoluteDefaultPose = avatarSkeleton->getAbsoluteDefaultPose(avatarHipsParentIndex); + + // the get the units and the heights for the animation and the avatar + const float animationUnitScale = extractScale(animModel.offset).y; + const float avatarUnitScale = extractScale(avatarSkeleton->getGeometryOffset()).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 + const float avatarHipsParentScale = avatarHipsParentAbsoluteDefaultPose.scale().y; + float animHipsParentScale = 1.0f; + const int animHipsParentIndex = animSkeleton.getParentIndex(animSkeleton.nameToJointIndex("Hips")); + // also, check to see if the animation hips have a scaled parent. + if (animHipsParentIndex >= 0) { + const AnimPose& animationHipsParentAbsoluteDefaultPose = animSkeleton.getAbsoluteDefaultPose(animHipsParentIndex); + animHipsParentScale = animationHipsParentAbsoluteDefaultPose.scale().y; + } + + // 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); + + boneLengthScale = avatarToAnimationHeightRatio * unitsRatio * parentScaleRatio; + } + } else { + + if (fabsf(glm::length(animZeroTrans)) > EPSILON) { + boneLengthScale = glm::length(avatarDefaultPose.trans()) / glm::length(animZeroTrans); + } } + AnimPose animTransPose = AnimPose(glm::vec3(1.0f), glm::quat(), avatarDefaultPose.trans() + boneLengthScale * (animTrans - animZeroTrans)); _anim[frame][avatarJointIndex] = animTransPose * animPreRotPose * animRotPose * animPostRotPose; + } } } diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index d710e9d8ff..8da2ddde3e 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(); diff --git a/libraries/animation/src/AnimSkeleton.cpp b/libraries/animation/src/AnimSkeleton.cpp index 03e3ac6ebd..f7b5fa8c83 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()); diff --git a/libraries/animation/src/AnimSkeleton.h b/libraries/animation/src/AnimSkeleton.h index 0eefbf973e..dcb35ac9cb 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; @@ -83,6 +84,7 @@ protected: std::vector _mirrorMap; QHash _jointIndicesByName; std::vector> _clusterBindMatrixOriginalValues; + glm::mat4 _geometryOffset; // no copies AnimSkeleton(const AnimSkeleton&) = delete; From 54f14b2772b9abc8fbdf54dbe77801a83330d97b Mon Sep 17 00:00:00 2001 From: amantley Date: Fri, 1 Mar 2019 12:41:05 -0800 Subject: [PATCH 68/87] added the case where my avatar has no parent of hips, but the animation does --- libraries/animation/src/AnimClip.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/libraries/animation/src/AnimClip.cpp b/libraries/animation/src/AnimClip.cpp index 109be27b58..da5e9b6508 100644 --- a/libraries/animation/src/AnimClip.cpp +++ b/libraries/animation/src/AnimClip.cpp @@ -153,11 +153,11 @@ void AnimClip::copyFromNetworkAnim() { const float EPSILON = 0.0001f; const int avatarHipsParentIndex = avatarSkeleton->getParentIndex(avatarSkeleton->nameToJointIndex("Hips")); - if (avatarJointIndex == avatarSkeleton->nameToJointIndex("Hips") && (avatarHipsParentIndex >= 0)) { + const int animHipsParentIndex = animSkeleton.getParentIndex(animSkeleton.nameToJointIndex("Hips")); + if (avatarJointIndex == avatarSkeleton->nameToJointIndex("Hips") && (avatarHipsParentIndex >= 0) || (animHipsParentIndex >= 0)) { const AnimPose& animHipsAbsoluteDefaultPose = animSkeleton.getAbsoluteDefaultPose(animSkeleton.nameToJointIndex("Hips")); const AnimPose& avatarHipsAbsoluteDefaultPose = avatarSkeleton->getAbsoluteDefaultPose(avatarSkeleton->nameToJointIndex("Hips")); - const AnimPose& avatarHipsParentAbsoluteDefaultPose = avatarSkeleton->getAbsoluteDefaultPose(avatarHipsParentIndex); // the get the units and the heights for the animation and the avatar const float animationUnitScale = extractScale(animModel.offset).y; @@ -166,10 +166,12 @@ void AnimClip::copyFromNetworkAnim() { const float animHeightInMeters = animationUnitScale * animHipsAbsoluteDefaultPose.trans().y; // get the parent scales for the avatar and the animation - const float avatarHipsParentScale = avatarHipsParentAbsoluteDefaultPose.scale().y; + float avatarHipsParentScale = 1.0f; float animHipsParentScale = 1.0f; - const int animHipsParentIndex = animSkeleton.getParentIndex(animSkeleton.nameToJointIndex("Hips")); - // also, check to see if the animation hips have a scaled parent. + if (avatarHipsParentIndex >= 0) { + const AnimPose& avatarHipsParentAbsoluteDefaultPose = avatarSkeleton->getAbsoluteDefaultPose(avatarHipsParentIndex); + avatarHipsParentScale = avatarHipsParentAbsoluteDefaultPose.scale().y; + } if (animHipsParentIndex >= 0) { const AnimPose& animationHipsParentAbsoluteDefaultPose = animSkeleton.getAbsoluteDefaultPose(animHipsParentIndex); animHipsParentScale = animationHipsParentAbsoluteDefaultPose.scale().y; From 8802eeadf654e4ef2411961e19c8997067c36088 Mon Sep 17 00:00:00 2001 From: amerhifi <43353902+amerhifi@users.noreply.github.com> Date: Fri, 1 Mar 2019 12:46:50 -0800 Subject: [PATCH 69/87] Make CI android build signed, don't fail on quest build fails (#18) merging build changes --- android/apps/interface/build.gradle | 13 +++++-------- android/apps/{questInterface => }/keystore.jks | Bin android/apps/questInterface/build.gradle | 8 ++++---- android/build_android.sh | 17 ++++++++++------- android/docker/Dockerfile | 2 +- 5 files changed, 20 insertions(+), 20 deletions(-) rename android/apps/{questInterface => }/keystore.jks (100%) diff --git a/android/apps/interface/build.gradle b/android/apps/interface/build.gradle index 4163df03b7..f954fbc1f4 100644 --- a/android/apps/interface/build.gradle +++ b/android/apps/interface/build.gradle @@ -66,10 +66,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 +90,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/questInterface/keystore.jks b/android/apps/keystore.jks similarity index 100% rename from android/apps/questInterface/keystore.jks rename to android/apps/keystore.jks diff --git a/android/apps/questInterface/build.gradle b/android/apps/questInterface/build.gradle index 43ce0c0c37..6f4f6b7441 100644 --- a/android/apps/questInterface/build.gradle +++ b/android/apps/questInterface/build.gradle @@ -44,10 +44,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' v2SigningEnabled false } } diff --git a/android/build_android.sh b/android/build_android.sh index 106a295750..e9c69b09de 100755 --- a/android/build_android.sh +++ b/android/build_android.sh @@ -4,16 +4,19 @@ set -xeuo pipefail ANDROID_BUILD_TYPE=release ANDROID_BUILD_TARGET=assembleRelease -case "$RELEASE_TYPE" in - PR) ANDROID_APK_SUFFIX=PR${RELEASE_NUMBER}-${SHA7}.apk ;; - *) ANDROID_APK_SUFFIX=${RELEASE_NUMBER}-${SHA7}.apk ;; -esac +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}-unsigned.apk +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} @@ -23,8 +26,8 @@ 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} -cp ${ANDROID_OUTPUT_DIR}/${ANDROID_OUTPUT_FILE} ./${ANDROID_APK_NAME} +./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 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 From 3e6061e4350684f88e2313cbed423a40a324821b Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 1 Mar 2019 13:45:00 -0800 Subject: [PATCH 70/87] try to not clear my avatar entities on domain switch --- interface/src/Application.cpp | 2 +- .../src/EntityTreeRenderer.cpp | 24 +++++++++---------- .../src/EntityTreeRenderer.h | 6 ++--- libraries/entities/src/EntityTree.cpp | 16 +++++++------ libraries/entities/src/EntityTree.h | 3 ++- libraries/entities/src/EntityTreeElement.cpp | 4 ++-- libraries/entities/src/EntityTreeElement.h | 2 +- libraries/octree/src/Octree.h | 2 +- libraries/octree/src/OctreeProcessor.cpp | 4 ++-- libraries/octree/src/OctreeProcessor.h | 2 +- 10 files changed, 34 insertions(+), 31 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 29d260cb5f..1a64378c36 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -6948,7 +6948,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(); diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index e54258fc3e..1e2c17fa8e 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -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,8 +214,8 @@ void EntityTreeRenderer::stopNonLocalEntityScripts() { } } -void EntityTreeRenderer::clearNonLocalEntities() { - stopNonLocalEntityScripts(); +void EntityTreeRenderer::clearDomainAndNonOwnedEntities() { + stopDomainAndNonOwnedEntities(); std::unordered_map savedEntities; // remove all entities from the scene @@ -225,7 +225,7 @@ void EntityTreeRenderer::clearNonLocalEntities() { 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; @@ -239,7 +239,7 @@ void EntityTreeRenderer::clearNonLocalEntities() { _layeredZones.clearNonLocalLayeredZones(); - OctreeProcessor::clearNonLocalEntities(); + OctreeProcessor::clearDomainAndNonOwnedEntities(); } void EntityTreeRenderer::clear() { @@ -655,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(); } } diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index 51568ab744..b4d3507c17 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -87,7 +87,7 @@ 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 @@ -170,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); @@ -179,7 +179,7 @@ private: QScriptValueList createEntityArgs(const EntityItemID& entityID); bool checkEnterLeaveEntities(); - void leaveNonLocalEntities(); + void leaveDomainAndNonOwnedEntities(); void leaveAllEntities(); void forceRecheckEntities(); diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 6e404ce690..cdf021638b 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -78,13 +78,14 @@ OctreeElementPointer EntityTree::createNewElement(unsigned char* octalCode) { return std::static_pointer_cast(newElement); } -void EntityTree::eraseNonLocalEntities() { +void EntityTree::eraseDomainAndNonOwnedEntities() { emit clearingEntities(); if (_simulation) { // local entities are not in the simulation, so we clear ALL _simulation->clearEntities(); } + this->withWriteLock([&] { QHash savedEntities; // NOTE: lock the Tree first, then lock the _entityMap. @@ -93,10 +94,10 @@ void EntityTree::eraseNonLocalEntities() { 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(); @@ -114,15 +115,16 @@ void EntityTree::eraseNonLocalEntities() { { 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; } } diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index dcce0e4b99..9da0ecedd8 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); } 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/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(); } From 618a1d5b8379c7303f195a81c97093082d5234e7 Mon Sep 17 00:00:00 2001 From: danteruiz Date: Fri, 1 Mar 2019 14:03:45 -0800 Subject: [PATCH 71/87] fix tablet button and gizmos rotation --- interface/src/ui/overlays/Overlays.cpp | 23 ++++++++++++----------- scripts/system/libraries/utils.js | 4 ---- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index 9b2f741531..08fa04245f 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -413,30 +413,31 @@ EntityItemProperties Overlays::convertOverlayToEntityProperties(QVariantMap& ove overlayProps["dimensions"] = vec3toVariant(ratio * dimensions); } - if (add || overlayProps.contains("rotation")) { + if (add && !overlayProps.contains("rotation") && !overlayProps.contains("localRotation")) { + glm::quat rotation; + overlayProps["rotation"] = quatToVariant(glm::angleAxis(-(float)M_PI_2, rotation * Vectors::RIGHT) * rotation); + } else if (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(); } } overlayProps["rotation"] = quatToVariant(glm::angleAxis(-(float)M_PI_2, rotation * Vectors::RIGHT) * rotation); - } - if (add || overlayProps.contains("localRotation")) { + + if (overlayProps.contains("localRotation")) { + auto iter = overlayProps.find("localRotation"); + if (iter != overlayProps.end()) { + overlayProps.erase(iter); + } + } + } else if (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(); } } overlayProps["localRotation"] = quatToVariant(glm::angleAxis(-(float)M_PI_2, rotation * Vectors::RIGHT) * rotation); diff --git a/scripts/system/libraries/utils.js b/scripts/system/libraries/utils.js index 6f74b43a8e..508e8d46e3 100644 --- a/scripts/system/libraries/utils.js +++ b/scripts/system/libraries/utils.js @@ -418,13 +418,11 @@ resizeTablet = function (width, newParentJointIndex, sensorToWorldScaleOverride) 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 }, - localRotation: { x: 0, y: 1, z: 0, w: 0 }, 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 }, - localRotation: { x: 0, y: 1, z: 0, w: 0 }, dimensions: { x: homeButtonDim, y: homeButtonDim, z: homeButtonDim } }); }; @@ -482,13 +480,11 @@ reparentAndScaleTablet = function(width, reparentProps) { 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 }, - localRotation: { x: 0, y: 1, z: 0, w: 0 }, 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 }, - localRotation: { x: 0, y: 1, z: 0, w: 0 }, dimensions: { x: homeButtonDim, y: homeButtonDim, z: homeButtonDim } }); } From c046b8ffd35c5aaffd953999eca5c3828d97b2c5 Mon Sep 17 00:00:00 2001 From: amantley Date: Fri, 1 Mar 2019 15:12:37 -0800 Subject: [PATCH 72/87] made is so the boneLengthScale is only computed once per animation clip --- libraries/animation/src/AnimClip.cpp | 80 +++++++++++++--------------- 1 file changed, 36 insertions(+), 44 deletions(-) diff --git a/libraries/animation/src/AnimClip.cpp b/libraries/animation/src/AnimClip.cpp index 9b87d058fd..4fe02e9307 100644 --- a/libraries/animation/src/AnimClip.cpp +++ b/libraries/animation/src/AnimClip.cpp @@ -123,6 +123,42 @@ void AnimClip::copyFromNetworkAnim() { 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 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); + + boneLengthScale = avatarToAnimationHeightRatio * unitsRatio * parentScaleRatio; + } + + for (int frame = 0; frame < animFrameCount; frame++) { const HFMAnimationFrame& animFrame = animModel.animationFrames[frame]; @@ -162,7 +198,6 @@ void AnimClip::copyFromNetworkAnim() { avatarSkeleton->convertAbsoluteRotationsToRelative(avatarRotations); _anim[frame].reserve(avatarJointCount); - for (int avatarJointIndex = 0; avatarJointIndex < avatarJointCount; avatarJointIndex++) { const AnimPose& avatarDefaultPose = avatarSkeleton->getRelativeDefaultPose(avatarJointIndex); @@ -177,49 +212,6 @@ void AnimClip::copyFromNetworkAnim() { // retarget translation from animation to avatar const glm::vec3& animZeroTrans = animModel.animationFrames[0].translations[animJointIndex]; - float boneLengthScale = 1.0f; - const float EPSILON = 0.0001f; - - const int avatarHipsParentIndex = avatarSkeleton->getParentIndex(avatarSkeleton->nameToJointIndex("Hips")); - const int animHipsParentIndex = animSkeleton.getParentIndex(animSkeleton.nameToJointIndex("Hips")); - if (avatarJointIndex == avatarSkeleton->nameToJointIndex("Hips") && (avatarHipsParentIndex >= 0) || (animHipsParentIndex >= 0)) { - - const AnimPose& animHipsAbsoluteDefaultPose = animSkeleton.getAbsoluteDefaultPose(animSkeleton.nameToJointIndex("Hips")); - const AnimPose& avatarHipsAbsoluteDefaultPose = avatarSkeleton->getAbsoluteDefaultPose(avatarSkeleton->nameToJointIndex("Hips")); - - // the get the units and the heights for the animation and the avatar - const float animationUnitScale = extractScale(animModel.offset).y; - const float avatarUnitScale = extractScale(avatarSkeleton->getGeometryOffset()).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; - float animHipsParentScale = 1.0f; - if (avatarHipsParentIndex >= 0) { - const AnimPose& avatarHipsParentAbsoluteDefaultPose = avatarSkeleton->getAbsoluteDefaultPose(avatarHipsParentIndex); - avatarHipsParentScale = avatarHipsParentAbsoluteDefaultPose.scale().y; - } - if (animHipsParentIndex >= 0) { - const AnimPose& animationHipsParentAbsoluteDefaultPose = animSkeleton.getAbsoluteDefaultPose(animHipsParentIndex); - animHipsParentScale = animationHipsParentAbsoluteDefaultPose.scale().y; - } - - // 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); - - boneLengthScale = avatarToAnimationHeightRatio * unitsRatio * parentScaleRatio; - } - } else { - - if (fabsf(glm::length(animZeroTrans)) > EPSILON) { - boneLengthScale = glm::length(avatarDefaultPose.trans()) / glm::length(animZeroTrans); - } - } - relativeTranslation = avatarDefaultPose.trans() + boneLengthScale * (animTrans - animZeroTrans); } else { // This joint is NOT in the animation at all. From 8e600adf1e5a997d785b67bd161cf200dfd7a935 Mon Sep 17 00:00:00 2001 From: danteruiz Date: Fri, 1 Mar 2019 15:30:11 -0800 Subject: [PATCH 73/87] making requested changes --- interface/src/ui/overlays/Overlays.cpp | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index 08fa04245f..8fef2d543d 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -414,32 +414,12 @@ EntityItemProperties Overlays::convertOverlayToEntityProperties(QVariantMap& ove } if (add && !overlayProps.contains("rotation") && !overlayProps.contains("localRotation")) { - glm::quat rotation; - overlayProps["rotation"] = quatToVariant(glm::angleAxis(-(float)M_PI_2, rotation * Vectors::RIGHT) * rotation); + overlayProps["rotation"] = quatToVariant(glm::angleAxis(-(float)M_PI_2, Vectors::RIGHT)); } else if (overlayProps.contains("rotation")) { - glm::quat rotation; - { - auto iter = overlayProps.find("rotation"); - if (iter != overlayProps.end()) { - rotation = quatFromVariant(iter.value()); - } - } + glm::quat rotation = quatFromVariant(overlayProps["rotation"]); overlayProps["rotation"] = quatToVariant(glm::angleAxis(-(float)M_PI_2, rotation * Vectors::RIGHT) * rotation); - - if (overlayProps.contains("localRotation")) { - auto iter = overlayProps.find("localRotation"); - if (iter != overlayProps.end()) { - overlayProps.erase(iter); - } - } } else if (overlayProps.contains("localRotation")) { - glm::quat rotation; - { - auto iter = overlayProps.find("localRotation"); - if (iter != overlayProps.end()) { - rotation = quatFromVariant(iter.value()); - } - } + glm::quat rotation = quatFromVariant(overlayProps["localRotation"]); overlayProps["localRotation"] = quatToVariant(glm::angleAxis(-(float)M_PI_2, rotation * Vectors::RIGHT) * rotation); } From da1ffc15e37ab6c22457a98f129df6d8ec7c425e Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 1 Mar 2019 15:41:57 -0800 Subject: [PATCH 74/87] lasers scale with avatar --- interface/src/raypick/LaserPointer.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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); } } From 884a64bfa6633a9c2029577e2582f9773fd3f99e Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Fri, 1 Mar 2019 15:50:50 -0800 Subject: [PATCH 75/87] fix resource crash --- libraries/networking/src/ResourceCache.cpp | 23 ++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/libraries/networking/src/ResourceCache.cpp b/libraries/networking/src/ResourceCache.cpp index 7345081380..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); - 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) { From 3f65c572586af8b4f854a6625a0674b89c08fa4a Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Fri, 1 Mar 2019 15:20:31 -0800 Subject: [PATCH 76/87] Fix CI release builds --- android/containerized_build.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/android/containerized_build.sh b/android/containerized_build.sh index 34e620ad2e..1ca597b2b9 100755 --- a/android/containerized_build.sh +++ b/android/containerized_build.sh @@ -9,6 +9,10 @@ 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 \ @@ -29,6 +33,7 @@ docker run \ -e OAUTH_CLIENT_ID \ -e OAUTH_REDIRECT_URI \ -e SHA7 \ + -e STABLE_BUILD \ -e VERSION_CODE \ "${DOCKER_IMAGE_NAME}" \ sh -c "./build_android.sh" From 12ffa41984730e9ac5ae24f9954368fd152dabb2 Mon Sep 17 00:00:00 2001 From: r3tk0n Date: Sat, 2 Mar 2019 13:55:06 -0800 Subject: [PATCH 77/87] Prevent HUD and Web Surface lasers from interrupting FarGrab. --- .../controllerModules/hudOverlayPointer.js | 16 +++++++++++++- .../controllerModules/webSurfaceLaserInput.js | 21 ++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) 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/webSurfaceLaserInput.js b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js index ec35dfe081..4f21b44533 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); From 708632ee82ce326c25dc7ee73e9b83fa63998673 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 4 Mar 2019 10:00:26 -0800 Subject: [PATCH 78/87] possible fix for model crash --- libraries/render-utils/src/Model.cpp | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 9bb3f72b31..683c517a15 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1346,14 +1346,18 @@ 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; + for (auto renderItem : _modelMeshRenderItemIDs) { + transaction.updateItem(renderItem, [&](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 From b2d08e9d42f152c4beff350b37842f4af93bcf0c Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Mon, 4 Mar 2019 15:33:21 -0700 Subject: [PATCH 79/87] apply axis rotation to translation and meshes --- libraries/fbx/src/FBXSerializer.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index e022ca8921..5246242a1e 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -1281,8 +1281,9 @@ 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.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) { @@ -1678,8 +1679,12 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } if (applyUpAxisZRotation) { - hfmModelPtr->meshExtents.rotate(upAxisZRotation); - hfmModelPtr->bindExtents.rotate(upAxisZRotation); + 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; } From 12f5a735d93e61e74c1824d1f5b0f9c88e3a2342 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 4 Mar 2019 14:50:09 -0800 Subject: [PATCH 80/87] simplifying mouse events and fix mouse on create --- interface/src/Application.cpp | 11 +- interface/src/ui/overlays/Overlays.cpp | 239 ++++++------------ interface/src/ui/overlays/Overlays.h | 17 +- .../src/EntityTreeRenderer.cpp | 34 +-- .../src/EntityTreeRenderer.h | 2 +- scripts/system/edit.js | 6 +- 6 files changed, 103 insertions(+), 206 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 2c8d71af00..e41d51de47 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4392,7 +4392,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 @@ -4426,14 +4425,10 @@ 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); + QUuid result = getEntities()->mousePressEvent(&mappedEvent); + setKeyboardFocusEntity(getEntities()->wantsKeyboardFocus(result) ? result : UNKNOWN_ENTITY_ID); } - std::pair overlayResult = getOverlays().mousePressEvent(&mappedEvent); - - QUuid focusedEntity = entityResult.first < overlayResult.first ? entityResult.second : overlayResult.second; - setKeyboardFocusEntity(getEntities()->wantsKeyboardFocus(focusedEntity) ? focusedEntity : UNKNOWN_ENTITY_ID); _controllerScriptingInterface->emitMousePressEvent(&mappedEvent); // send events to any registered scripts @@ -4476,7 +4471,6 @@ void Application::mouseDoublePressEvent(QMouseEvent* event) { if (!_controllerScriptingInterface->areEntityClicksCaptured()) { getEntities()->mouseDoublePressEvent(&mappedEvent); } - getOverlays().mouseDoublePressEvent(&mappedEvent); // if one of our scripts have asked to capture this event, then stop processing it if (_controllerScriptingInterface->isMouseCaptured()) { @@ -4501,7 +4495,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 diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index 5ae3f7d38e..dfd698f6c5 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"; @@ -81,13 +73,23 @@ void Overlays::cleanupAllOverlays() { void Overlays::init() { auto entityScriptingInterface = DependencyManager::get().data(); - auto pointerManager = DependencyManager::get(); - connect(pointerManager.data(), &PointerManager::hoverBeginOverlay, entityScriptingInterface , &EntityScriptingInterface::hoverEnterEntity); - connect(pointerManager.data(), &PointerManager::hoverContinueOverlay, entityScriptingInterface, &EntityScriptingInterface::hoverOverEntity); - connect(pointerManager.data(), &PointerManager::hoverEndOverlay, entityScriptingInterface, &EntityScriptingInterface::hoverLeaveEntity); - connect(pointerManager.data(), &PointerManager::triggerBeginOverlay, entityScriptingInterface, &EntityScriptingInterface::mousePressOnEntity); - connect(pointerManager.data(), &PointerManager::triggerContinueOverlay, entityScriptingInterface, &EntityScriptingInterface::mouseMoveOnEntity); - connect(pointerManager.data(), &PointerManager::triggerEndOverlay, entityScriptingInterface, &EntityScriptingInterface::mouseReleaseOnEntity); + 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) { @@ -1159,7 +1161,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) { @@ -1206,57 +1208,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() { - RayToOverlayIntersectionResult overlayResult; - overlayResult.intersects = false; - auto pickResult = DependencyManager::get()->getPrevPickResultTyped(DependencyManager::get()->getMouseRayPickID()); - if (pickResult) { - overlayResult.intersects = pickResult->type == IntersectionType::LOCAL_ENTITY; - 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); + } } } @@ -1264,7 +1275,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); + } } } @@ -1272,113 +1286,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(); - 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(); - 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(); - 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(); - 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); } } diff --git a/interface/src/ui/overlays/Overlays.h b/interface/src/ui/overlays/Overlays.h index 93efc2bc0b..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,9 +714,6 @@ private: PointerEvent calculateOverlayPointerEvent(const QUuid& id, const PickRay& ray, const RayToOverlayIntersectionResult& rayPickResult, QMouseEvent* event, PointerEvent::EventType eventType); - QUuid _currentClickingOnOverlayID; - QUuid _currentHoverOverOverlayID; - static QString entityToOverlayType(const QString& type); static QString overlayToEntityType(const QString& type); static std::unordered_map _entityToOverlayTypes; @@ -732,12 +724,17 @@ private: 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/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index e54258fc3e..c7457c6443 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -73,14 +73,14 @@ 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) { @@ -93,10 +93,10 @@ EntityTreeRenderer::EntityTreeRenderer(bool wantScripts, AbstractViewStateInterf 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->isVisible() && entity->getType() == EntityTypes::Web) { @@ -106,8 +106,8 @@ EntityTreeRenderer::EntityTreeRenderer(bool wantScripts, AbstractViewStateInterf 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->isVisible() && entity->getType() == EntityTypes::Web) { @@ -792,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"); @@ -827,10 +827,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) { diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index 51568ab744..8cc34cda10 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -94,7 +94,7 @@ public: 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); diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 9d807264aa..bce308d5fc 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -924,11 +924,7 @@ var toolBar = (function () { that.setActive = function (active) { ContextOverlay.enabled = !active; Settings.setValue(EDIT_SETTING, active); - if (active) { - Controller.captureEntityClickEvents(); - } else { - Controller.releaseEntityClickEvents(); - + if (!active) { closeExistingDialogWindow(); } if (active === isActive) { From fd88ec0d16caafe1bef84a4287b3b9e1b5cec846 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 4 Mar 2019 15:25:02 -0800 Subject: [PATCH 81/87] need to copy meshStates on main thread --- libraries/render-utils/src/Model.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 683c517a15..dd9b0280ca 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1347,9 +1347,10 @@ void Model::updateRig(float deltaTime, glm::mat4 parentTransform) { void Model::computeMeshPartLocalBounds() { render::Transaction transaction; + auto meshStates = _meshStates; for (auto renderItem : _modelMeshRenderItemIDs) { - transaction.updateItem(renderItem, [&](ModelMeshPartPayload& data) { - const Model::MeshState& state = _meshStates.at(data._meshIndex); + transaction.updateItem(renderItem, [this, meshStates](ModelMeshPartPayload& data) { + const Model::MeshState& state = meshStates.at(data._meshIndex); if (_useDualQuaternionSkinning) { data.computeAdjustedLocalBound(state.clusterDualQuaternions); } else { From f2c248c0a26a56b8c7969a14e274422524322cf3 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 4 Mar 2019 17:08:09 -0800 Subject: [PATCH 82/87] disable href and entity script events when in edit mode --- interface/src/Application.cpp | 15 +++++++-------- .../entities-renderer/src/EntityTreeRenderer.cpp | 12 +++++++----- libraries/entities/src/EntityTree.cpp | 8 ++++++++ libraries/entities/src/EntityTree.h | 4 ++++ libraries/script-engine/src/ScriptEngine.cpp | 4 +++- scripts/system/edit.js | 6 +++++- 6 files changed, 34 insertions(+), 15 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index e41d51de47..c85ced5dee 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1061,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 @@ -4425,10 +4429,8 @@ void Application::mousePressEvent(QMouseEvent* event) { #endif QMouseEvent mappedEvent(event->type(), transformedPos, event->screenPos(), event->button(), event->buttons(), event->modifiers()); - if (!_controllerScriptingInterface->areEntityClicksCaptured()) { - QUuid result = getEntities()->mousePressEvent(&mappedEvent); - setKeyboardFocusEntity(getEntities()->wantsKeyboardFocus(result) ? result : 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 @@ -4467,10 +4469,7 @@ void Application::mouseDoublePressEvent(QMouseEvent* event) { transformedPos, event->screenPos(), event->button(), event->buttons(), event->modifiers()); - - if (!_controllerScriptingInterface->areEntityClicksCaptured()) { - getEntities()->mouseDoublePressEvent(&mappedEvent); - } + getEntities()->mouseDoublePressEvent(&mappedEvent); // if one of our scripts have asked to capture this event, then stop processing it if (_controllerScriptingInterface->isMouseCaptured()) { diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index c7457c6443..576137390e 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -805,11 +805,13 @@ QUuid 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); diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 6e404ce690..644fe0620e 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -2972,6 +2972,7 @@ QStringList EntityTree::getJointNames(const QUuid& entityID) const { std::function EntityTree::_getEntityObjectOperator = nullptr; std::function EntityTree::_textSizeOperator = nullptr; +std::function EntityTree::_areEntityClicksCapturedOperator = nullptr; QObject* EntityTree::getEntityObject(const QUuid& id) { if (_getEntityObjectOperator) { @@ -2987,6 +2988,13 @@ 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 diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index dcce0e4b99..01829df7f8 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -268,6 +268,9 @@ public: 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, @@ -378,6 +381,7 @@ private: static std::function _getEntityObjectOperator; static std::function _textSizeOperator; + static std::function _areEntityClicksCapturedOperator; std::vector _staleProxies; 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/scripts/system/edit.js b/scripts/system/edit.js index bce308d5fc..9d807264aa 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -924,7 +924,11 @@ var toolBar = (function () { that.setActive = function (active) { ContextOverlay.enabled = !active; Settings.setValue(EDIT_SETTING, active); - if (!active) { + if (active) { + Controller.captureEntityClickEvents(); + } else { + Controller.releaseEntityClickEvents(); + closeExistingDialogWindow(); } if (active === isActive) { From 29a308dcaacfa138b721d183a146b38befe3988a Mon Sep 17 00:00:00 2001 From: amer cerkic Date: Tue, 5 Mar 2019 10:07:33 -0800 Subject: [PATCH 83/87] adding modelScale initialization so that it does not fail the validScale check assert --- libraries/entities/src/ModelEntityItem.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/entities/src/ModelEntityItem.cpp b/libraries/entities/src/ModelEntityItem.cpp index 505ee26c0f..87d80ee044 100644 --- a/libraries/entities/src/ModelEntityItem.cpp +++ b/libraries/entities/src/ModelEntityItem.cpp @@ -40,6 +40,7 @@ ModelEntityItem::ModelEntityItem(const EntityItemID& entityItemID) : EntityItem( _type = EntityTypes::Model; _lastKnownCurrentFrame = -1; _visuallyReady = false; + _modelScale=glm::vec3(1.0f); } const QString ModelEntityItem::getTextures() const { From 36e9c604e9e70efde65c5a7f64044d0da052a022 Mon Sep 17 00:00:00 2001 From: amer cerkic Date: Tue, 5 Mar 2019 10:45:15 -0800 Subject: [PATCH 84/87] fixed based on comment --- libraries/entities/src/ModelEntityItem.cpp | 1 - libraries/entities/src/ModelEntityItem.h | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/libraries/entities/src/ModelEntityItem.cpp b/libraries/entities/src/ModelEntityItem.cpp index 87d80ee044..505ee26c0f 100644 --- a/libraries/entities/src/ModelEntityItem.cpp +++ b/libraries/entities/src/ModelEntityItem.cpp @@ -40,7 +40,6 @@ ModelEntityItem::ModelEntityItem(const EntityItemID& entityItemID) : EntityItem( _type = EntityTypes::Model; _lastKnownCurrentFrame = -1; _visuallyReady = false; - _modelScale=glm::vec3(1.0f); } const QString ModelEntityItem::getTextures() const { diff --git a/libraries/entities/src/ModelEntityItem.h b/libraries/entities/src/ModelEntityItem.h index 0efbbbb16c..89acd6179b 100644 --- a/libraries/entities/src/ModelEntityItem.h +++ b/libraries/entities/src/ModelEntityItem.h @@ -164,7 +164,7 @@ protected: int _lastKnownCurrentFrame{-1}; glm::u8vec3 _color; - glm::vec3 _modelScale; + glm::vec3 _modelScale {1.0f}; QString _modelURL; bool _relayParentJoints; bool _groupCulled { false }; From fab343a1d4d703009c585aca2bc7eba64e983cfe Mon Sep 17 00:00:00 2001 From: amer cerkic Date: Tue, 5 Mar 2019 10:52:54 -0800 Subject: [PATCH 85/87] correcting spacing --- libraries/entities/src/ModelEntityItem.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/entities/src/ModelEntityItem.h b/libraries/entities/src/ModelEntityItem.h index 89acd6179b..08468617ba 100644 --- a/libraries/entities/src/ModelEntityItem.h +++ b/libraries/entities/src/ModelEntityItem.h @@ -164,7 +164,7 @@ protected: int _lastKnownCurrentFrame{-1}; glm::u8vec3 _color; - glm::vec3 _modelScale {1.0f}; + glm::vec3 _modelScale { 1.0f }; QString _modelURL; bool _relayParentJoints; bool _groupCulled { false }; From 59200103af72ac2689b3d54443ef6b2c27224a5d Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Wed, 6 Mar 2019 07:06:48 +0100 Subject: [PATCH 86/87] Not for sale explanation --- .../qml/hifi/avatarPackager/AvatarProject.qml | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index a92739cf8a..e1945fad90 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -256,7 +256,7 @@ Item { name: "errors" PropertyChanges { target: doctorStatusMessage - text: "It seems your project has a few issues that will affect how it works in High Fidelity. " + text: "Your avatar has a few issues." } } ] @@ -306,6 +306,29 @@ 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.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 From a84a43320c875a05ffcd632802b021a42ae38e39 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Wed, 6 Mar 2019 19:15:01 +0100 Subject: [PATCH 87/87] added spacing between lines --- .../resources/qml/hifi/avatarPackager/AvatarProject.qml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index e1945fad90..bf8c06d1b3 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -318,6 +318,7 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.top: infoMessage.bottom + anchors.topMargin: 10 anchors.bottomMargin: 24 @@ -338,8 +339,8 @@ Item { visible: AvatarPackagerCore.currentAvatarProject && AvatarPackagerCore.currentAvatarProject.hasErrors anchors { - top: infoMessage.bottom - topMargin: 28 + top: notForSaleMessage.bottom + topMargin: 16 horizontalCenter: parent.horizontalCenter }